From 0091b1064e56d772d5ea1182d1e02e684de85d4b Mon Sep 17 00:00:00 2001 From: "pramod.mahajan" Date: Fri, 17 Oct 2025 13:49:44 +0530 Subject: [PATCH] intergrated collection managment --- src/components/collections/AddPayment.jsx | 266 +++++++++++ src/components/collections/CollectionList.jsx | 296 ++++++++++++ src/components/collections/Comment.jsx | 85 ++++ .../collections/ManageCollection.jsx | 445 ++++++++++++++++++ .../collections/PaymentHistoryTable.jsx | 53 +++ src/components/collections/ViewCollection.jsx | 258 ++++++++++ .../collections/collectionSchema.jsx | 101 ++++ src/components/common/AccessDenied.jsx | 22 + src/pages/collections/CollectionPage.jsx | 223 +++++++++ src/router/AppRoutes.jsx | 2 + src/utils/constants.jsx | 45 +- 11 files changed, 1767 insertions(+), 29 deletions(-) create mode 100644 src/components/collections/AddPayment.jsx create mode 100644 src/components/collections/CollectionList.jsx create mode 100644 src/components/collections/Comment.jsx create mode 100644 src/components/collections/ManageCollection.jsx create mode 100644 src/components/collections/PaymentHistoryTable.jsx create mode 100644 src/components/collections/ViewCollection.jsx create mode 100644 src/components/collections/collectionSchema.jsx create mode 100644 src/components/common/AccessDenied.jsx create mode 100644 src/pages/collections/CollectionPage.jsx diff --git a/src/components/collections/AddPayment.jsx b/src/components/collections/AddPayment.jsx new file mode 100644 index 00000000..a47200ee --- /dev/null +++ b/src/components/collections/AddPayment.jsx @@ -0,0 +1,266 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { defaultPayment, paymentSchema } from "./collectionSchema"; +import Label from "../common/Label"; +import DatePicker from "../common/DatePicker"; +import { formatDate } from "date-fns"; +import { useCollectionContext } from "../../pages/collections/CollectionPage"; +import { useAddPayment, useCollection } from "../../hooks/useCollections"; +import { formatFigure, localToUtc } from "../../utils/appUtils"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import Avatar from "../common/Avatar"; +import { PaymentHistorySkeleton } from "./CollectionSkeleton"; +import { usePaymentAjustmentHead } from "../../hooks/masterHook/useMaster"; + +const AddPayment = ({ onClose }) => { + const { addPayment } = useCollectionContext(); + const { data, isLoading, isError, error } = useCollection( + addPayment?.invoiceId + ); + const { + data: paymentTypes, + isLoading: isPaymentTypeLoading, + isError: isPaymentTypeError, + error: paymentError, + } = usePaymentAjustmentHead(true); + const methods = useForm({ + resolver: zodResolver(paymentSchema), + defaultValues: defaultPayment, + }); + const { + control, + register, + handleSubmit, + reset, + formState: { errors }, + } = methods; + const { mutate: AddPayment, isPending } = useAddPayment(() => { + handleClose(); + }); + const onSubmit = (formData) => { + const payload = { + ...formData, + paymentReceivedDate: localToUtc(formData.paymentReceivedDate), + invoiceId: addPayment.invoiceId, + }; + AddPayment(payload); + }; + const handleClose = (formData) => { + reset(defaultPayment); + }; + + return ( +
+
Add Payment
+ +
+
+
+ + + {errors.transactionId && ( + + {errors.transactionId.message} + + )} +
+ +
+ + + {errors.paymentReceivedDate && ( + + {errors.paymentReceivedDate.message} + + )} +
+ +
+ + + {errors.paymentAdjustmentHeadId && ( + + {errors.paymentAdjustmentHeadId.message} + + )} +
+ +
+ + + {errors.amount && ( + {errors.amount.message} + )} +
+
+ + + {errors.description && ( + + {errors.description.message} + + )} +
+
+ +
+ + +
document.getElementById("attachments").click()} + > + + + Click to select or click here to browse + + + (PDF, JPG, PNG,Doc,docx,xls,xlsx max 5MB) + + + { + onFileChange(e); + e.target.value = ""; + }} + /> +
+ {errors.attachments && ( + + {errors.attachments.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.attachments) && + errors.attachments.map((fileError, index) => ( +
+ { + (fileError?.fileSize?.message || + fileError?.contentType?.message || + fileError?.base64Data?.message, + fileError?.documentId?.message) + } +
+ ))} +
+ +
+ {" "} + + +
+
+ + + + ); +}; + +export default ManageCollection; diff --git a/src/components/collections/PaymentHistoryTable.jsx b/src/components/collections/PaymentHistoryTable.jsx new file mode 100644 index 00000000..e7b31fc9 --- /dev/null +++ b/src/components/collections/PaymentHistoryTable.jsx @@ -0,0 +1,53 @@ +import React from 'react' +import { formatUTCToLocalTime } from '../../utils/dateUtils' +import { formatFigure } from '../../utils/appUtils' +import Avatar from '../common/Avatar' + +const PaymentHistoryTable = ({data}) => { + return ( +
+ + {data?.receivedInvoicePayments?.length > 0 ? ( +
+ + + + + + + + + + + + + {data.receivedInvoicePayments.map((payment, index) => ( + + + + + + + + + ))} + +
Sr.NoTransaction ID Received Date Payment Adjustment-HeadAmountUpdated By
{index + 1}{payment.transactionId}{formatUTCToLocalTime(payment.paymentReceivedDate)}{payment?.paymentAdjustmentHead?.name ?? "--"} + {formatFigure(payment.amount, { + type: "currency", + currency: "INR", + })} + +
+ + {payment.createdBy?.firstName}{" "} + {payment.createdBy?.lastName} +
+
+
+ ):(

No History

)} +
+ ) +} + +export default PaymentHistoryTable diff --git a/src/components/collections/ViewCollection.jsx b/src/components/collections/ViewCollection.jsx new file mode 100644 index 00000000..799033e5 --- /dev/null +++ b/src/components/collections/ViewCollection.jsx @@ -0,0 +1,258 @@ +import React, { useState } from "react"; +import { useCollectionContext } from "../../pages/collections/CollectionPage"; +import { useCollection } from "../../hooks/useCollections"; +import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import { formatFigure, getIconByFileType } from "../../utils/appUtils"; +import Avatar from "../common/Avatar"; +import PaymentHistoryTable from "./PaymentHistoryTable"; +import Comment from "./Comment"; +import { CollectionDetailsSkeleton } from "./CollectionSkeleton"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import { ADMIN_COLLECTION, EDIT_COLLECTION } from "../../utils/constants"; + +const ViewCollection = ({ onClose }) => { + const [activeTab, setActiveTab] = useState("payments"); + const isAdmin = useHasUserPermission(ADMIN_COLLECTION); + const canEditCollection = useHasUserPermission(EDIT_COLLECTION); + const { viewCollection, setCollection, setDocumentView } = + useCollectionContext(); + const { data, isLoading, isError, error } = useCollection(viewCollection); + + const handleEdit = () => { + setCollection({ isOpen: true, invoiceId: viewCollection }); + onClose(); + }; + + if (isLoading) return ; + if (isError) return
{error.message}
; + + return ( +
+

Collection Details

+
+
+
+ +
{data?.project?.name}
+
+
+ {" "} + + {data?.isActive ? "Active" : "Inactive"} + + {(isAdmin || canEditCollection) && + !data?.receivedInvoicePayments && ( + + + + )} +
+
+
+
+
Title :
+
{data?.title}
+
+
+
+
+
Invoice Number:
+
{data?.invoiceNumber}
+
+
+ {/* Row 2: E-Invoice Number + Project */} +
+
+
E-Invoice Number:
+
{data?.eInvoiceNumber}
+
+
+ + {/* Row 3: Invoice Date + Client Submitted Date */} +
+
+
Invoice Date:
+
+ {formatUTCToLocalTime(data?.invoiceDate)} +
+
+
+
+
+
Client Submission Date:
+
+ {formatUTCToLocalTime(data?.clientSubmitedDate)} +
+
+
+ {/* Row 4: Expected Payment Date + Mark as Completed */} +
+
+
Expected Payment Date:
+
+ {formatUTCToLocalTime(data?.exceptedPaymentDate)} +
+
+
+ + {/* Row 5: Basic Amount + Tax Amount */} +
+
+
Basic Amount :
+
+ {formatFigure(data?.basicAmount, { + type: "currency", + currency: "INR", + })} +
+
+
+
+
+
Tax Amount :
+
+ {formatFigure(data?.taxAmount, { + type: "currency", + currency: "INR", + })} +
+
+
+ {/* Row 6: Balance Amount + Created At */} +
+
+
Balance Amount :
+
+ {formatFigure(data?.balanceAmount, { + type: "currency", + currency: "INR", + })} +
+
+
+
+
+
Created At :
+
{formatUTCToLocalTime(data?.createdAt)}
+
+
+ {/* Row 7: Created By */} +
+
+
Created By :
+
+ + + {data?.createdBy?.firstName} {data?.createdBy?.lastName} + +
+
+
+ {/* Description */} +
+
Description :
+
{data?.description}
+
+ +
+ + +
+ {data?.attachments?.map((doc) => { + const isImage = doc.contentType?.includes("image"); + + return ( +
{ + if (isImage) { + setDocumentView({ + IsOpen: true, + Image: doc.preSignedUrl, + }); + } + }} + > + + + {doc.fileName} + +
+ ); + }) ?? "No Attachment"} +
+
+ +
+ {/* Tabs Navigation */} +
    +
  • + +
  • +
  • + +
  • +
+ + {/* Tab Content */} +
+ {activeTab === "payments" && ( +
+ +
+ )} + + {activeTab === "details" && ( +
+ +
+ )} +
+
+
+
+ ); +}; + +export default ViewCollection; diff --git a/src/components/collections/collectionSchema.jsx b/src/components/collections/collectionSchema.jsx new file mode 100644 index 00000000..bb97fede --- /dev/null +++ b/src/components/collections/collectionSchema.jsx @@ -0,0 +1,101 @@ +import { z } from "zod"; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const ALLOWED_TYPES = [ + "application/pdf", + "application/doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "image/png", + "image/jpg", + "image/jpeg", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", +]; + + +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", + }), + attachments: 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: "", + attachments: [], +}; + +export const paymentSchema = z.object({ + paymentReceivedDate: z.string().min(1, { message: "Date is required" }), + transactionId: z.string().min(1, "Transaction ID is required"), + amount: z.number().min(1, "Amount must be greater than zero"), + comment:z.string().min(1,{message:"Comment required"}), + paymentAdjustmentHeadId:z.string().min(1,{message:"Payment Type required"}) +}); + +// Default Value +export const defaultPayment = { + paymentReceivedDate: null, + transactionId: "", + amount: 0, + comment:"", + paymentAdjustmentHeadId:"" +}; + + +export const CommentSchema = z.object({ + comment:z.string().min(1,{message:"Comment required"}) +}) diff --git a/src/components/common/AccessDenied.jsx b/src/components/common/AccessDenied.jsx new file mode 100644 index 00000000..62cc5f4e --- /dev/null +++ b/src/components/common/AccessDenied.jsx @@ -0,0 +1,22 @@ +import React from "react"; +import Breadcrumb from "./Breadcrumb"; + +const AccessDenied = ({data}) => { + return ( +
+ + +
+ +

+ Access Denied: You don't have permission to perform this action ! +

+
+ +
+ ); +}; + +export default AccessDenied; diff --git a/src/pages/collections/CollectionPage.jsx b/src/pages/collections/CollectionPage.jsx new file mode 100644 index 00000000..7617d40a --- /dev/null +++ b/src/pages/collections/CollectionPage.jsx @@ -0,0 +1,223 @@ +import React, { createContext, useContext, 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"; +import ConfirmModal from "../../components/common/ConfirmModal"; +import showToast from "../../services/toastService"; +import { useMarkedPaymentReceived } from "../../hooks/useCollections"; +import GlobalModel from "../../components/common/GlobalModel"; +import AddPayment from "../../components/collections/AddPayment"; +import ViewCollection from "../../components/collections/ViewCollection"; +import ManageCollection from "../../components/collections/ManageCollection"; +import PreviewDocument from "../../components/Expenses/PreviewDocument"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; +import { + ADDPAYMENT_COLLECTION, + ADMIN_COLLECTION, + CREATE_COLLECTION, + EDIT_COLLECTION, + VIEW_COLLECTION, +} from "../../utils/constants"; +import AccessDenied from "../../components/common/AccessDenied"; + +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 [viewCollection, setViewCollection] = useState(null); + const [makeCollection, setCollection] = useState({ + isOpen: false, + invoiceId: null, + }); + const [ViewDocument, setDocumentView] = useState({ + IsOpen: false, + Image: null, + }); + const [processedPayment, setProcessedPayment] = useState(null); + const [addPayment, setAddPayment] = useState({ + isOpen: false, + invoiceId: null, + }); + const [showPending, setShowPending] = useState(false); + const [searchText, setSearchText] = useState(""); + const isAdmin = useHasUserPermission(ADMIN_COLLECTION); + const canViewCollection = useHasUserPermission(VIEW_COLLECTION); + const canCreate = useHasUserPermission(CREATE_COLLECTION); + const canEditCollection = useHasUserPermission(EDIT_COLLECTION); + const canAddPayment = useHasUserPermission(ADDPAYMENT_COLLECTION); + const methods = useForm({ + defaultValues: { + 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, + setCollection, + setAddPayment, + addPayment, + setViewCollection, + viewCollection, + setDocumentView, + }; + const { mutate: MarkedReceived, isPending } = useMarkedPaymentReceived(() => { + setProcessedPayment(null); + }); + const handleMarkedPayment = (payload) => { + MarkedReceived(payload); + }; + if (isAdmin === undefined || + canAddPayment === undefined || + canEditCollection === undefined || + canViewCollection === undefined || + canCreate === undefined +) { + return
Checking access...
; +} + +if (!isAdmin && !canAddPayment && !canEditCollection && !canViewCollection && !canCreate) { + return ; +} + return ( + +
+ + +
+
+
+ + + +
+
+
+ setShowPending(e.target.checked)} + /> + +
+
+ +
+
+ {" "} + setSearchText(e.target.value)} + placeholder="search Collection" + className="form-control form-control-sm" + /> +
+ { (canCreate || isAdmin) && ( + +)} + +
+
+
+ + + + {makeCollection.isOpen && ( + setCollection({ isOpen: false, invoiceId: null })} + > + setCollection({ isOpen: false, invoiceId: null })} + /> + + )} + + {addPayment.isOpen && ( + setAddPayment({ isOpen: false, invoiceId: null })} + > + setAddPayment({ isOpen: false, invoiceId: null })} + /> + + )} + + {viewCollection && ( + setViewCollection(null)} + > + setViewCollection(null)} /> + + )} + + {ViewDocument.IsOpen && ( + setDocumentView({ IsOpen: false, Image: null })} + > + + + )} + + handleMarkedPayment(processedPayment?.invoiceId)} + onClose={() => setProcessedPayment(null)} + /> +
+
+ ); +}; + +export default CollectionPage; \ No newline at end of file diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 17aba35c..1a1ee3c5 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -53,6 +53,7 @@ import TenantSelectionPage from "../pages/authentication/TenantSelectionPage"; import DailyProgrssReport from "../pages/DailyProgressReport/DailyProgrssReport"; import ProjectPage from "../pages/project/ProjectPage"; import { ComingSoonPage } from "../pages/Misc/ComingSoonPage"; +import CollectionPage from "../pages/collections/CollectionPage"; const router = createBrowserRouter( [ { @@ -95,6 +96,7 @@ const router = createBrowserRouter( { path: "/activities/reports", element: }, { path: "/gallary", element: }, { path: "/expenses/:status?/:project?", element: }, + { path: "/collection", element: }, { path: "/expenses", element: }, { path: "/masters", element: }, { path: "/tenants", element: }, diff --git a/src/utils/constants.jsx b/src/utils/constants.jsx index d43d8055..7ddccf56 100644 --- a/src/utils/constants.jsx +++ b/src/utils/constants.jsx @@ -1,13 +1,12 @@ -export const BASE_URL = process.env.VITE_BASE_URL; - -// export const BASE_URL = "https://api.marcoaiot.com"; - - export const THRESH_HOLD = 48; // hours export const DURATION_TIME = 10; // minutes export const ITEMS_PER_PAGE = 20; export const OTP_EXPIRY_SECONDS = 300; // OTP time +export const BASE_URL = process.env.VITE_BASE_URL; + +// export const BASE_URL = "https://api.marcoaiot.com"; + export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323"; export const VIEW_MASTER = "5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"; @@ -50,7 +49,7 @@ export const DIRECTORY_ADMIN = "4286a13b-bb40-4879-8c6d-18e9e393beda"; export const DIRECTORY_MANAGER = "62668630-13ce-4f52-a0f0-db38af2230c5"; export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb"; - +// ========================Finance========================================================= // -----------------------Expense---------------------------------------- export const VIEW_SELF_EXPENSE = "385be49f-8fde-440e-bdbc-3dffeb8dd116"; @@ -66,6 +65,16 @@ export const PROCESS_EXPENSE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; export const EXPENSE_MANAGE = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; +// --------------------------------Collection---------------------------- + +export const ADMIN_COLLECTION = "dbf17591-09fe-4c93-9e1a-12db8f5cc5de"; +export const VIEW_COLLECTION = "c8d7eea5-4033-4aad-9ebe-76de49896830"; +export const CREATE_COLLECTION = "b93141fd-dbd3-4051-8f57-bf25d18e3555"; +export const EDIT_COLLECTION = "455187b4-fef1-41f9-b3d0-025d0b6302c3"; +export const ADDPAYMENT_COLLECTION = "061d9ccd-85b4-4cb0-be06-2f9f32cebb72"; + +// ========================================================================================== + export const EXPENSE_REJECTEDBY = [ "d1ee5eec-24b6-4364-8673-a8f859c60729", "965eda62-7907-4963-b4a1-657fb0b2724b", @@ -145,26 +154,4 @@ export const PROJECT_STATUS = [ label: "Completed", }, ]; - - -export const EXPENSE_STATUS = { - daft:"297e0d8f-f668-41b5-bfea-e03b354251c8", - review_pending:"6537018f-f4e9-4cb3-a210-6c3b2da999d7", - payment_pending:"f18c5cfd-7815-4341-8da2-2c2d65778e27", - approve_pending:"4068007f-c92f-4f37-a907-bc15fe57d4d8", - process_pending:"61578360-3a49-4c34-8604-7b35a3787b95" - -} - -export const UUID_REGEX = - /^\/employee\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; - -export const ALLOW_PROJECTSTATUS_ID = [ - "603e994b-a27f-4e5d-a251-f3d69b0498ba", - "cdad86aa-8a56-4ff4-b633-9c629057dfef", - "b74da4c2-d07e-46f2-9919-e75e49b12731", -]; - -export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000"; - - +export const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000"; \ No newline at end of file