From 376a2a397f487a65c6fa38cbc520854d64ed4d66 Mon Sep 17 00:00:00 2001 From: "pramod.mahajan" Date: Tue, 14 Oct 2025 00:21:19 +0530 Subject: [PATCH] intergrated get list and create collection --- src/ModalProvider.jsx | 3 + src/components/collections/CollectionList.jsx | 196 ++++++++- src/components/collections/NewCollection.jsx | 391 ++++++++++++++++++ .../collections/collectionSchema.jsx | 67 +++ src/hooks/useCollections.js | 58 +++ src/pages/collections/CollectionPage.jsx | 104 ++++- src/repositories/ColllectionRepository.jsx | 20 + 7 files changed, 815 insertions(+), 24 deletions(-) create mode 100644 src/components/collections/NewCollection.jsx create mode 100644 src/components/collections/collectionSchema.jsx create mode 100644 src/hooks/useCollections.js create mode 100644 src/repositories/ColllectionRepository.jsx diff --git a/src/ModalProvider.jsx b/src/ModalProvider.jsx index c4e0779f..26fd1821 100644 --- a/src/ModalProvider.jsx +++ b/src/ModalProvider.jsx @@ -4,17 +4,20 @@ import OrganizationModal from "./components/Organization/OrganizationModal"; import { useAuthModal, useModal } from "./hooks/useAuth"; import SwitchTenant from "./pages/authentication/SwitchTenant"; import ChangePasswordPage from "./pages/authentication/ChangePassword"; +import NewCollection from "./components/collections/NewCollection"; const ModalProvider = () => { const { isOpen, onClose } = useOrganizationModal(); const { isOpen: isAuthOpen } = useAuthModal(); const {isOpen:isChangePass} = useModal("ChangePassword") + const {isOpen:isCollectionNew} = useModal("newCollection"); return ( <> {isOpen && } {isAuthOpen && } {isChangePass && } + {isCollectionNew && } ); }; diff --git a/src/components/collections/CollectionList.jsx b/src/components/collections/CollectionList.jsx index 941e0a37..6151b723 100644 --- a/src/components/collections/CollectionList.jsx +++ b/src/components/collections/CollectionList.jsx @@ -1,11 +1,193 @@ -import React from 'react' +import React, { useState } from "react"; +import { useCollections } from "../../hooks/useCollections"; +import { ITEMS_PER_PAGE } from "../../utils/constants"; +import { formatFigure, localToUtc, useDebounce } from "../../utils/appUtils"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import Pagination from "../common/Pagination"; + +const CollectionList = ({ fromDate, toDate, isPending, searchString }) => { + const [currentPage, setCurrentPage] = useState(1); + const searchDebounce = useDebounce(searchString, 500); + + const { data, isLoading, isError, error } = useCollections( + ITEMS_PER_PAGE, + currentPage, + localToUtc(fromDate), + localToUtc(toDate), + isPending, + true, + searchDebounce + ); + + const paginate = (page) => { + if (page >= 1 && page <= (data?.totalPages ?? 1)) { + setCurrentPage(page); + } + }; + + const collectionColumns = [ + { + key: "invoiceDate", + label: "Invoice Date", + getValue: (col) => ( + + {formatUTCToLocalTime(col.invoiceDate)} + + ), + align: "text-start", + }, + { + key: "invoiceId", + label: "Invoice Id", + getValue: (col) => ( + + {col?.invoiceNumber ?? "-"} + + ), + align: "text-start", + }, + { + key: "project", + label: "Project", + getValue: (col) => ( + + {col?.project?.name ?? "-"} + + ), + align: "text-start", + }, + { + key: "submittedDate", + label: "Submitted Date", + getValue: (col) => ( + + {formatUTCToLocalTime(col.createdAt)} + + ), + align: "text-start", + }, + { + key: "expectedSubmittedDate", + label: "Expected Payment Date", + getValue: (col) => ( + + {formatUTCToLocalTime(col.exceptedPaymentDate) ?? "-"} + + ), + align: "text-start", + }, + { + key: "amount", + label: "Amount", + getValue: (col) => ( + + {formatFigure(col?.basicAmount, { type: "currency", currency: "INR" }) ?? 0} + + ), + align: "text-end", + }, + { + key: "balance", + label: "Balance", + getValue: (col) => ( + + {formatFigure(col?.balanceAmount, { type: "currency", currency: "INR" }) ?? 0} + + ), + align: "text-end", + }, + ]; + + if (isLoading) return

Loading...

; + if (isError) return

{error.message}

; -const CollectionList = () => { return ( -
- +
+
+
+ + + + {collectionColumns.map((col) => ( + + ))} + + + + + {Array.isArray(data?.data) && data.data.length > 0 ? ( + data.data.map((row, i) => ( + + {collectionColumns.map((col) => ( + + ))} + + + )) + ) : ( + + + + )} + +
+ {col.label} + + Action +
+ {col.getValue(row)} + +
+ + + +
+
+ No Collections Found +
+ {data?.data?.length > 0 && ( +
+ +
+ )} +
+
- ) -} + ); +}; -export default CollectionList +export default CollectionList; diff --git a/src/components/collections/NewCollection.jsx b/src/components/collections/NewCollection.jsx new file mode 100644 index 00000000..e1d10226 --- /dev/null +++ b/src/components/collections/NewCollection.jsx @@ -0,0 +1,391 @@ +import React from "react"; +import { useModal } from "../../hooks/useAuth"; +import Modal from "../common/Modal"; +import { FormProvider, useForm } from "react-hook-form"; +import Label from "../common/Label"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { defaultCollection, newCollection } from "./collectionSchema"; +import SelectMultiple from "../common/SelectMultiple"; +import { useProjectName } from "../../hooks/useProjects"; +import DatePicker from "../common/DatePicker"; +import { useCreateCollection } from "../../hooks/useCollections"; +import { formatFileSize, localToUtc } from "../../utils/appUtils"; + +const NewCollection = ({ collectionId }) => { + const { onClose, onOpen, isOpen } = useModal("newCollection"); + const { projectNames, projectLoading } = useProjectName(); + console.log(projectNames); + const methods = useForm({ + resolver: zodResolver(newCollection), + defaultValues: defaultCollection, + }); + const { + control, + watch, + register, + setValue, + handleSubmit, + reset, + formState: { errors }, + } = methods; + + const { mutate: createNewCollection, isPending } = useCreateCollection(()=>{ + handleClose() + }); + + const files = watch("billAttachments"); + const onFileChange = async (e) => { + const newFiles = Array.from(e.target.files); + if (newFiles.length === 0) return; + + const existingFiles = watch("billAttachments") || []; + + const parsedFiles = await Promise.all( + newFiles.map(async (file) => { + const base64Data = await toBase64(file); + return { + fileName: file.name, + base64Data, + contentType: file.type, + fileSize: file.size, + description: "", + isActive: true, + }; + }) + ); + + const combinedFiles = [ + ...existingFiles, + ...parsedFiles.filter( + (newFile) => + !existingFiles.some( + (f) => + f.fileName === newFile.fileName && f.fileSize === newFile.fileSize + ) + ), + ]; + + setValue("billAttachments", combinedFiles, { + shouldDirty: true, + shouldValidate: true, + }); + }; + + const toBase64 = (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result.split(",")[1]); + reader.onerror = (error) => reject(error); + }); + const removeFile = (index) => { + if (collectionId) { + const newFiles = files.map((file, i) => { + if (file.documentId !== index) return file; + return { + ...file, + isActive: false, + }; + }); + setValue("billAttachments", newFiles, { shouldValidate: true }); + } else { + const newFiles = files.filter((_, i) => i !== index); + setValue("billAttachments", newFiles, { shouldValidate: true }); + } + }; + + const onSubmit = (formData) => { + const payload = { + ...formData, + clientSubmitedDate: localToUtc(formData.clientSubmitedDate), + invoiceDate: localToUtc(formData.invoiceDate), + exceptedPaymentDate: localToUtc(formData.exceptedPaymentDate), + }; + createNewCollection(payload); + }; + const handleClose = () => { + reset(defaultCollection); + onClose(); + }; + + const bodyContent = ( +
+
New Collection
+ +
+
+
+ + + {errors.title && ( + {errors.title.message} + )} +
+
+ + + {errors.invoiceId && ( + + {errors.invoiceId.message} + + )} +
+
+ + + {errors.invoiceId && ( + + {errors.invoiceId.message} + + )} +
+
+ + + {errors.invoiceDate && ( + + {errors.invoiceDate.message} + + )} +
+
+ + + {errors.exceptedPaymentDate && ( + + {errors.exceptedPaymentDate.message} + + )} +
+
+ + + {errors.exceptedPaymentDate && ( + + {errors.exceptedPaymentDate.message} + + )} +
+
+ + + {errors.projectId && ( + + {errors.projectId.message} + + )} +
+
+ + + {errors.basicAmount && ( + + {errors.basicAmount.message} + + )} +
+
+ + + {errors.taxAmount && ( + + {errors.taxAmount.message} + + )} +
+
+
+ + + {errors.description && ( + + {errors.description.message} + + )} +
+
+ +
+ + +
+ document.getElementById("billAttachments").click() + } + > + + + Click to select or click here to browse + + (PDF, JPG, PNG, max 5MB) + + { + onFileChange(e); + e.target.value = ""; + }} + /> +
+ {errors.billAttachments && ( + + {errors.billAttachments.message} + + )} + {files.length > 0 && ( +
+ {files + .filter((file) => { + if (collectionId) { + return file.isActive; + } + return true; + }) + .map((file, idx) => ( + +
+ + {file.fileName} + + + {file.fileSize ? formatFileSize(file.fileSize) : ""} + +
+ { + e.preventDefault(); + removeFile(collectionId ? file.documentId : idx); + }} + > +
+ ))} +
+ )} + + {Array.isArray(errors.billAttachments) && + errors.billAttachments.map((fileError, index) => ( +
+ { + (fileError?.fileSize?.message || + fileError?.contentType?.message || + fileError?.base64Data?.message, + fileError?.documentId?.message) + } +
+ ))} +
+ +
+ {" "} + + +
+
+
+
+
+ ); + return ( + + ); +}; + +export default NewCollection; diff --git a/src/components/collections/collectionSchema.jsx b/src/components/collections/collectionSchema.jsx new file mode 100644 index 00000000..06a432ca --- /dev/null +++ b/src/components/collections/collectionSchema.jsx @@ -0,0 +1,67 @@ +import { z } from "zod"; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_TYPES = [ + "application/pdf", + "image/png", + "image/jpg", + "image/jpeg", +]; + +export const newCollection = z.object({ + title: z.string().trim().min(1, { message: "Title is required" }), + projectId: z.string().trim().min(1, { message: "Project is required" }), + invoiceDate: z.string().min(1, { message: "Date is required" }), + description: z.string().trim().optional(), + clientSubmitedDate: z.string().min(1, { message: "Date is required" }), + exceptedPaymentDate: z.string().min(1, { message: "Date is required" }), + invoiceNumber: z.string().trim().min(1, { message: "Invoice is required" }).max(17,{message:"Invalid Number"}), + eInvoiceNumber: z.string().trim().min(1, { message: "E-Invoice No is required" }), + taxAmount: z.coerce + .number({ + invalid_type_error: "Amount is required and must be a number", + }) + .min(1, "Amount must be Enter") + .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { + message: "Amount must have at most 2 decimal places", + }), + basicAmount: z.coerce + .number({ + invalid_type_error: "Amount is required and must be a number", + }) + .min(1, "Amount must be Enter") + .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { + message: "Amount must have at most 2 decimal places", + }), + billAttachments: z + .array( + z.object({ + fileName: z.string().min(1, { message: "Filename is required" }), + base64Data: z.string().nullable(), + contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), { + message: "Only PDF, PNG, JPG, or JPEG files are allowed", + }), + documentId: z.string().optional(), + fileSize: z.number().max(MAX_FILE_SIZE, { + message: "File size must be less than or equal to 5MB", + }), + description: z.string().optional(), + isActive: z.boolean().default(true), + }) + ) + .nonempty({ message: "At least one file attachment is required" }), +}); + +export const defaultCollection = { + projectId: "", + invoiceNumber: " ", + eInvoiceNumber: "", + title: "", + clientSubmitedDate: null, + invoiceDate: null, + exceptedPaymentDate: null, + taxAmount: "", + basicAmount:"", + description: "", + billAttachments: [], +}; diff --git a/src/hooks/useCollections.js b/src/hooks/useCollections.js new file mode 100644 index 00000000..d2403f3b --- /dev/null +++ b/src/hooks/useCollections.js @@ -0,0 +1,58 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { CollectionRepository } from "../repositories/ColllectionRepository"; +import showToast from "../services/toastService"; + +export const useCollections = ( + pageSize, + pageNumber, + fromDate, + toDate, + isPending, + isActive, + searchString +) => { + return useQuery({ + queryKey: [ + "collections", + pageSize, + pageNumber, + fromDate, + toDate, + isPending, + isActive, + searchString, + ], + + queryFn: async () => { + const response = await CollectionRepository.getCollections( + pageSize, + pageNumber, + fromDate, + toDate, + isPending, + isActive, + searchString + ); + return response.data; + }, + + keepPreviousData: true, + staleTime: 1000 * 60 * 1, + }); +}; + +export const useCreateCollection = (onSuccessCallBack) => { + return useMutation({ + mutationFn: async (payload) => + await CollectionRepository.createNewCollection(payload), + onSuccess: (_, variables) => { + showToast("New Collection created Successfully", "success"); + if (onSuccessCallBack) onSuccessCallBack(); + }, + onError: (error) => { + showToast( + error.message || error.response.data.message || "Something Went wrong" + ); + }, + }); +}; diff --git a/src/pages/collections/CollectionPage.jsx b/src/pages/collections/CollectionPage.jsx index 6ee6cd64..9512ff99 100644 --- a/src/pages/collections/CollectionPage.jsx +++ b/src/pages/collections/CollectionPage.jsx @@ -1,21 +1,91 @@ -import React from 'react' -import Breadcrumb from '../../components/common/Breadcrumb' +import React, { useState } from "react"; +import moment from "moment"; +import Breadcrumb from "../../components/common/Breadcrumb"; +import CollectionList from "../../components/collections/CollectionList"; +import { useModal } from "../../hooks/useAuth"; +import { FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { DateRangePicker1 } from "../../components/common/DateRangePicker"; +import { isPending } from "@reduxjs/toolkit"; const CollectionPage = () => { + const { onOpen } = useModal("newCollection"); + const [showPending, setShowPending] = useState(false); + const [searchText, setSearchText] = useState(""); + const methods = useForm({ + defaultValues: { + fromDate: moment().subtract(6, "days").format("DD-MM-YYYY"), + toDate: moment().format("DD-MM-YYYY"), + }, + }); + const { watch } = methods; + const [fromDate, toDate] = watch(["fromDate", "toDate"]); + const handleToggleActive = (e) => setShowPending(e.target.checked); return ( -
- -
-
-
- -
-
-
-
- ) -} +
+ -export default CollectionPage +
+
+
+ + + +
+
+
+ setShowPending(e.target.checked)} + /> + +
+
+ +
+
+ {" "} + setSearchText(e.target.value)} + placeholder="search Collection" + className="form-control form-control-sm" + /> +
+ +
+
+
+ + +
+ ); +}; + +export default CollectionPage; diff --git a/src/repositories/ColllectionRepository.jsx b/src/repositories/ColllectionRepository.jsx new file mode 100644 index 00000000..f0f885ef --- /dev/null +++ b/src/repositories/ColllectionRepository.jsx @@ -0,0 +1,20 @@ +import { api } from "../utils/axiosClient"; +import { DirectoryRepository } from "./DirectoryRepository"; + +export const CollectionRepository = { + createNewCollection: (data) => + api.post(`/api/Collection/invoice/create`, data), + getCollections: (pageSize, pageNumber,fromDate,toDate, isPending,isActive, searchString) => { + let url = `/api/Collection/invoice/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isPending=${isPending}&isActive=${isActive}&searchString=${searchString}`; + + const params = []; + if (fromDate) params.push(`fromDate=${fromDate}`); + if (toDate) params.push(`toDate=${toDate}`); + + if (params.length > 0) { + url += `&${params.join("&")}`; + } + return api.get(url); + }, +}; +