diff --git a/public/assets/css/core-extend.css b/public/assets/css/core-extend.css index 2e9e51ef..1ecc882c 100644 --- a/public/assets/css/core-extend.css +++ b/public/assets/css/core-extend.css @@ -10,7 +10,21 @@ .table_header_border { border-bottom:2px solid var(--bs-table-border-color) ; } +.text-gary-80 { +color:var(--bs-gray-500) +} +.text-royalblue{ +color: #1796e3; +} + +.text-md { +font-size: 2rem; +} + +.text-md-b { +font-weight: normal; +} .text-xxs { font-size: 0.55rem; } /* 8px */ .text-xs { font-size: 0.75rem; } /* 12px */ diff --git a/src/ModalProvider.jsx b/src/ModalProvider.jsx index c4e0779f..bb217139 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/ManageCollection"; 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/Expenses/PreviewDocument.jsx b/src/components/Expenses/PreviewDocument.jsx index 9c0f5c49..ce554ca9 100644 --- a/src/components/Expenses/PreviewDocument.jsx +++ b/src/components/Expenses/PreviewDocument.jsx @@ -1,27 +1,53 @@ -import { useState } from 'react'; +import { useState } from "react"; const PreviewDocument = ({ imageUrl }) => { const [loading, setLoading] = useState(true); + const [rotation, setRotation] = useState(0); return ( -
+ <> +
+ setRotation((prev) => prev + 90)} + > +
+
+ {loading && ( -
- Loading... -
+
Loading...
)} - Full View setLoading(false)} - /> + +
+ Full View setLoading(false)} + /> +
+ +
+ +
+ ); }; diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx index ae72921a..3c4b5f86 100644 --- a/src/components/Layout/Header.jsx +++ b/src/components/Layout/Header.jsx @@ -34,6 +34,8 @@ const Header = () => { const isDashboardPath = /^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname); const isProjectPath = /^\/projects$/.test(location.pathname); + const isCollectionPath = + /^\/collection$/.test(location.pathname) || /^\/$/.test(location.pathname); const showProjectDropdown = (pathname) => { const isDirectoryPath = /^\/directory$/.test(pathname); @@ -216,7 +218,7 @@ const Header = () => { className="dropdown-menu" style={{ overflow: "auto", maxHeight: "300px" }} > - {isDashboardPath && ( + {(isDashboardPath|| isCollectionPath) &&(
  • + +
  • + + + + + ); +}; + +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/components/common/Avatar.jsx b/src/components/common/Avatar.jsx index 5c3f25e6..04b36f3c 100644 --- a/src/components/common/Avatar.jsx +++ b/src/components/common/Avatar.jsx @@ -51,7 +51,7 @@ const Avatar = ({ firstName, lastName, size = "sm", classAvatar }) => { return (
    -
    +
    {generateAvatarText(firstName, lastName)} 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/components/master/MasterModal.jsx b/src/components/master/MasterModal.jsx index b26d6de4..569ea854 100644 --- a/src/components/master/MasterModal.jsx +++ b/src/components/master/MasterModal.jsx @@ -16,10 +16,10 @@ import ManageDocumentCategory from "./ManageDocumentCategory"; import ManageDocumentType from "./ManageDocumentType"; import ManageServices from "./Services/ManageServices"; import ServiceGroups from "./Services/ServicesGroups"; +import ManagePaymentHead from "./paymentAdjustmentHead/ManagePaymentHead"; const MasterModal = ({ modaldata, closeModal }) => { if (!modaldata?.modalType || modaldata.modalType === "delete") { - return null; } @@ -33,7 +33,7 @@ const MasterModal = ({ modaldata, closeModal }) => { ), "Job Role": , - "Edit-Job Role": , + "Edit-Job Role": , "Work Category": , "Edit-Work Category": , "Contact Category": , @@ -58,24 +58,20 @@ const MasterModal = ({ modaldata, closeModal }) => { "Edit-Document Type": ( ), - "Services": ( - - ), - "Edit-Services": ( - - ), - "Manage-Services": ( - - ), + Services: , + "Edit-Services": , + "Manage-Services": , + "Payment Adjustment Head": , + "Edit-Payment Adjustment Head": }; return ( -
    -
    -

    {`${masterType, " ", modalType}`}

    +
    +
    +

    {`${(masterType, " ", modalType)}`}

    +
    + {modalComponents[modalType] || null}
    - { modalComponents[modalType] || null} -
    ); }; diff --git a/src/components/master/paymentAdjustmentHead/ManagePaymentHead.jsx b/src/components/master/paymentAdjustmentHead/ManagePaymentHead.jsx new file mode 100644 index 00000000..1afba4ab --- /dev/null +++ b/src/components/master/paymentAdjustmentHead/ManagePaymentHead.jsx @@ -0,0 +1,107 @@ +import React, { useEffect } from "react"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Label from "../../common/Label"; +import { useCreatePaymentAjustmentHead, useUpdatePaymentAjustmentHead } from "../../../hooks/masterHook/useMaster"; + +export const simpleFormSchema = z.object({ + name: z.string().min(1, "Name is required"), + description: z.string().min(1, "Description is required"), +}); + +const ManagePaymentHead = ({ data, onClose }) => { + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(simpleFormSchema), + defaultValues: { + name: "", + description: "", + }, + }); + + const {mutate:CreateAjustmentHead,isPending} = useCreatePaymentAjustmentHead(()=>{ + handleClose?.() + }); + const {mutate:UpdateAjustmentHead,isPending:isUpdating} = useUpdatePaymentAjustmentHead(()=>{ + handleClose?.() + }) + const onSubmit = (formData) => { + if(data){ + let id = data?.id; + const payload = { + ...formData, + id:id, + } + UpdateAjustmentHead({id:id,payload:payload}) + }else{ + let payload={ + ...formData + } + CreateAjustmentHead(payload) + } + }; + + useEffect(() => { + if (data) { + reset({ + name: data.name, + description: data.description, + }); + } + }, [data]); + const handleClose = () => { + reset(); + onClose(); + }; + return ( +
    +
    +
    + + + {errors.name && ( +
    {errors.name.message}
    + )} +
    + +
    + +