From 376a2a397f487a65c6fa38cbc520854d64ed4d66 Mon Sep 17 00:00:00 2001 From: "pramod.mahajan" Date: Tue, 14 Oct 2025 00:21:19 +0530 Subject: [PATCH 1/2] 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); + }, +}; + From 9288ac1fbcb080fe7af6124d65ab138a2b171191 Mon Sep 17 00:00:00 2001 From: "pramod.mahajan" Date: Tue, 14 Oct 2025 10:24:45 +0530 Subject: [PATCH 2/2] successfuuly fetch data and create collection --- src/components/collections/AddPayment.jsx | 11 ++ src/components/collections/CollectionList.jsx | 58 +++++- src/components/collections/NewCollection.jsx | 19 +- .../collections/collectionSchema.jsx | 30 +++- src/components/common/ConfirmModal.jsx | 32 ++-- src/components/common/DateRangePicker.jsx | 50 +++--- src/hooks/useCollections.js | 24 ++- src/pages/collections/CollectionPage.jsx | 169 ++++++++++++------ src/repositories/ColllectionRepository.jsx | 4 + 9 files changed, 278 insertions(+), 119 deletions(-) create mode 100644 src/components/collections/AddPayment.jsx diff --git a/src/components/collections/AddPayment.jsx b/src/components/collections/AddPayment.jsx new file mode 100644 index 00000000..71b87edf --- /dev/null +++ b/src/components/collections/AddPayment.jsx @@ -0,0 +1,11 @@ +import React from 'react' + +const AddPayment = () => { + return ( +
+ +
+ ) +} + +export default AddPayment diff --git a/src/components/collections/CollectionList.jsx b/src/components/collections/CollectionList.jsx index 6151b723..72ef34b1 100644 --- a/src/components/collections/CollectionList.jsx +++ b/src/components/collections/CollectionList.jsx @@ -4,6 +4,7 @@ import { ITEMS_PER_PAGE } from "../../utils/constants"; import { formatFigure, localToUtc, useDebounce } from "../../utils/appUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils"; import Pagination from "../common/Pagination"; +import { useCollectionContext } from "../../pages/collections/CollectionPage"; const CollectionList = ({ fromDate, toDate, isPending, searchString }) => { const [currentPage, setCurrentPage] = useState(1); @@ -18,6 +19,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => { true, searchDebounce ); + const {setProcessedPayment} = useCollectionContext() const paginate = (page) => { if (page >= 1 && page <= (data?.totalPages ?? 1)) { @@ -99,7 +101,10 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => { className="text-truncate d-inline-block" style={{ maxWidth: "200px" }} > - {formatFigure(col?.basicAmount, { type: "currency", currency: "INR" }) ?? 0} + {formatFigure(col?.basicAmount, { + type: "currency", + currency: "INR", + }) ?? 0} ), align: "text-end", @@ -112,7 +117,10 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => { className="text-truncate d-inline-block py-3" style={{ maxWidth: "200px" }} > - {formatFigure(col?.balanceAmount, { type: "currency", currency: "INR" }) ?? 0} + {formatFigure(col?.balanceAmount, { + type: "currency", + currency: "INR", + }) ?? 0} ), align: "text-end", @@ -155,10 +163,48 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => { className="sticky-action-column text-center" style={{ padding: "12px 8px" }} > -
- - - +
+ + +
diff --git a/src/components/collections/NewCollection.jsx b/src/components/collections/NewCollection.jsx index e1d10226..5e3480cb 100644 --- a/src/components/collections/NewCollection.jsx +++ b/src/components/collections/NewCollection.jsx @@ -11,10 +11,9 @@ 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 NewCollection = ({ collectionId, onClose }) => { + const { projectNames, projectLoading } = useProjectName(); - console.log(projectNames); const methods = useForm({ resolver: zodResolver(newCollection), defaultValues: defaultCollection, @@ -108,12 +107,13 @@ const NewCollection = ({ collectionId }) => { onClose(); }; - const bodyContent = ( -
+ return ( +
+
New Collection
-
+
{
+ ); - return ( - - ); + + + }; export default NewCollection; diff --git a/src/components/collections/collectionSchema.jsx b/src/components/collections/collectionSchema.jsx index 06a432ca..36510552 100644 --- a/src/components/collections/collectionSchema.jsx +++ b/src/components/collections/collectionSchema.jsx @@ -15,8 +15,15 @@ export const newCollection = z.object({ 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" }), + 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", @@ -25,7 +32,7 @@ export const newCollection = z.object({ .refine((val) => /^\d+(\.\d{1,2})?$/.test(val.toString()), { message: "Amount must have at most 2 decimal places", }), - basicAmount: z.coerce + basicAmount: z.coerce .number({ invalid_type_error: "Amount is required and must be a number", }) @@ -61,7 +68,22 @@ export const defaultCollection = { invoiceDate: null, exceptedPaymentDate: null, taxAmount: "", - basicAmount:"", + basicAmount: "", description: "", billAttachments: [], }; + +export const paymentSchema = z.object({ + invoiceId: z.string().min(1, { message: "Invoice Id required" }), + paymentReceivedDate: z.string().datetime(), + transactionId: z.string().min(1, "Transaction ID is required"), + amount: z.number().min(1, "Amount must be greater than zero"), +}); + +// Default Value +export const defaultPayment = { + invoiceId: "", + paymentReceivedDate: null, + transactionId: "", + amount: 0, +}; diff --git a/src/components/common/ConfirmModal.jsx b/src/components/common/ConfirmModal.jsx index 037c6a59..dd0158eb 100644 --- a/src/components/common/ConfirmModal.jsx +++ b/src/components/common/ConfirmModal.jsx @@ -13,19 +13,18 @@ const ConfirmModal = ({ if (!isOpen) return null; const TypeofIcon = () => { - if (type === "delete") { - return ( - - ); + switch (type) { + case "delete": + return ; + case "success": + return ; + case "warning": + return ; + default: + return null; } - return null; }; - const modalSize = type === "delete" ? "sm" : "md"; - return (
-
+
{header && {header}}
-
{TypeofIcon()}
-
+
+ {TypeofIcon()} +
+
{message}
+
diff --git a/src/components/common/DateRangePicker.jsx b/src/components/common/DateRangePicker.jsx index 3a93853d..737a4a19 100644 --- a/src/components/common/DateRangePicker.jsx +++ b/src/components/common/DateRangePicker.jsx @@ -88,7 +88,6 @@ export default DateRangePicker; - export const DateRangePicker1 = ({ startField = "startDate", endField = "endDate", @@ -98,6 +97,7 @@ export const DateRangePicker1 = ({ resetSignal, defaultRange = true, maxDate = null, + howManyDay = 6, ...rest }) => { const inputRef = useRef(null); @@ -107,12 +107,13 @@ export const DateRangePicker1 = ({ field: { ref }, } = useController({ name: startField, control }); + // Apply default range const applyDefaultDates = () => { const today = new Date(); - const past = new Date(); - past.setDate(today.getDate() - 6); + const past = new Date(today.getTime()); + past.setDate(today.getDate() - howManyDay); - const format = (d) => flatpickr.formatDate(d, "d-m-Y"); + const format = (d) => window.flatpickr.formatDate(d, "d-m-Y"); const formattedStart = format(past); const formattedEnd = format(today); @@ -127,15 +128,19 @@ export const DateRangePicker1 = ({ useEffect(() => { if (!inputRef.current || inputRef.current._flatpickr) return; - const instance = flatpickr(inputRef.current, { + if (defaultRange && !getValues(startField) && !getValues(endField)) { + applyDefaultDates(); + } + + const instance = window.flatpickr(inputRef.current, { mode: "range", dateFormat: "d-m-Y", allowInput: allowText, - maxDate , + maxDate, onChange: (selectedDates) => { if (selectedDates.length === 2) { const [start, end] = selectedDates; - const format = (d) => flatpickr.formatDate(d, "d-m-Y"); + const format = (d) => window.flatpickr.formatDate(d, "d-m-Y"); setValue(startField, format(start), { shouldValidate: true }); setValue(endField, format(end), { shouldValidate: true }); } else { @@ -148,12 +153,10 @@ export const DateRangePicker1 = ({ const currentStart = getValues(startField); const currentEnd = getValues(endField); - if (defaultRange && !currentStart && !currentEnd) { - applyDefaultDates(); - } else if (currentStart && currentEnd) { + if (currentStart && currentEnd) { instance.setDate([ - flatpickr.parseDate(currentStart, "d-m-Y"), - flatpickr.parseDate(currentEnd, "d-m-Y"), + window.flatpickr.parseDate(currentStart, "d-m-Y"), + window.flatpickr.parseDate(currentEnd, "d-m-Y"), ]); } @@ -161,20 +164,19 @@ export const DateRangePicker1 = ({ }, []); useEffect(() => { - if (resetSignal !== undefined) { - if (defaultRange) { - applyDefaultDates(); - } else { - setValue(startField, "", { shouldValidate: true }); - setValue(endField, "", { shouldValidate: true }); + if (resetSignal !== undefined) { + if (defaultRange) { + applyDefaultDates(); + } else { + setValue(startField, "", { shouldValidate: true }); + setValue(endField, "", { shouldValidate: true }); - if (inputRef.current?._flatpickr) { - inputRef.current._flatpickr.clear(); + if (inputRef.current?._flatpickr) { + inputRef.current._flatpickr.clear(); + } } } - } -}, [resetSignal, defaultRange, setValue, startField, endField]); - + }, [resetSignal, defaultRange, setValue, startField, endField]); const start = getValues(startField); const end = getValues(endField); @@ -186,7 +188,7 @@ export const DateRangePicker1 = ({ type="text" className="form-control form-control-sm" placeholder={placeholder} - defaultValue={formattedValue} + value={formattedValue} ref={(el) => { inputRef.current = el; ref(el); diff --git a/src/hooks/useCollections.js b/src/hooks/useCollections.js index d2403f3b..7c1b3776 100644 --- a/src/hooks/useCollections.js +++ b/src/hooks/useCollections.js @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CollectionRepository } from "../repositories/ColllectionRepository"; import showToast from "../services/toastService"; @@ -42,16 +42,36 @@ export const useCollections = ( }; export const useCreateCollection = (onSuccessCallBack) => { + const clinent = useQueryClient(); return useMutation({ mutationFn: async (payload) => await CollectionRepository.createNewCollection(payload), onSuccess: (_, variables) => { showToast("New Collection created Successfully", "success"); + clinent.invalidateQueries({ queryKey: ["collections"] }); if (onSuccessCallBack) onSuccessCallBack(); }, onError: (error) => { showToast( - error.message || error.response.data.message || "Something Went wrong" + error.response.data.message || error.message || "Something Went wrong" + ); + }, + }); +}; + +export const useMarkedPaymentReceived = (onSuccessCallBack) => { + const client = useQueryClient(); + return useMutation({ + mutationFn: async (payload) => + await CollectionRepository.markPaymentReceived(payload), + onSuccess: async () => { + client.invalidateQueries({ queryKey: ["collections"] }); + showToast("Payment Received marked Successfully", "success"); + if (onSuccessCallBack) onSuccessCallBack(); + }, + onError: (error) => { + showToast( + error.response.data.message || error.message || "Something Went wrong" ); }, }); diff --git a/src/pages/collections/CollectionPage.jsx b/src/pages/collections/CollectionPage.jsx index 9512ff99..79ff9780 100644 --- a/src/pages/collections/CollectionPage.jsx +++ b/src/pages/collections/CollectionPage.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { createContext, useContext, useState } from "react"; import moment from "moment"; import Breadcrumb from "../../components/common/Breadcrumb"; import CollectionList from "../../components/collections/CollectionList"; @@ -7,84 +7,135 @@ import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { DateRangePicker1 } from "../../components/common/DateRangePicker"; import { isPending } from "@reduxjs/toolkit"; +import ConfirmModal from "../../components/common/ConfirmModal"; +import showToast from "../../services/toastService"; +import { useMarkedPaymentReceived } from "../../hooks/useCollections"; +import NewCollection from "../../components/collections/NewCollection"; +import GlobalModel from "../../components/common/GlobalModel"; +const CollectionContext = createContext(); +export const useCollectionContext = () => { + const context = useContext(CollectionContext); + if (!context) { + window.location = "/dashboard"; + showToast("Out of Context Happend inside Collection Context", "warning"); + } + return context; +}; const CollectionPage = () => { const { onOpen } = useModal("newCollection"); + const [makeCollection, setCollection] = useState({ + isOpen: false, + invoiceId: null, + }); + const [processedPayment, setProcessedPayment] = useState(null); const [showPending, setShowPending] = useState(false); const [searchText, setSearchText] = useState(""); const methods = useForm({ defaultValues: { - fromDate: moment().subtract(6, "days").format("DD-MM-YYYY"), + fromDate: moment().subtract(180, "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); + + const contextMassager = { + setProcessedPayment, + }; + const { mutate: MarkedReceived, isPending } = useMarkedPaymentReceived(() => { + setProcessedPayment(null); + }); + const handleMarkedPayment = () => {}; return ( -
- + +
+ -
-
-
- - - -
-
-
- setShowPending(e.target.checked)} - /> -
-
- -
+ + + {makeCollection.isOpen && ( + setCollection({ isOpen: false, invoiceId: null })} + > + setCollection({ isOpen: false, invoiceId: null })} + /> + + )} + + setProcessedPayment(null)} + /> +
+ ); }; diff --git a/src/repositories/ColllectionRepository.jsx b/src/repositories/ColllectionRepository.jsx index f0f885ef..7f51da60 100644 --- a/src/repositories/ColllectionRepository.jsx +++ b/src/repositories/ColllectionRepository.jsx @@ -16,5 +16,9 @@ export const CollectionRepository = { } return api.get(url); }, + + makeReceivePayment:(data)=> api.post(`/api/Collection/invoice/payment/received`), + markPaymentReceived:(invoiceId)=>api.put(`/api/Collection/invoice/marked/completed/${invoiceId}`) + };