setTimeout(() => setShowSuggestions(false), 150)}
@@ -50,7 +51,6 @@ const InputSuggestions = ({
{filteredList.map((org) => (
handleSelectSuggestion(org)}
- className={`dropdown-item ${
- org === value ? "active" : ""
+ className={`dropdown-item ${org === value ? "active" : ""
}`}
>
{org}
))}
-
+
)}
{error &&
{error}}
diff --git a/src/components/common/SigleFileUploader.jsx b/src/components/common/SigleFileUploader.jsx
new file mode 100644
index 00000000..eb32b304
--- /dev/null
+++ b/src/components/common/SigleFileUploader.jsx
@@ -0,0 +1,111 @@
+import { useRef } from "react";
+import Label from "./Label";
+import Tooltip from "./Tooltip";
+import { formatFileSize, getIconByFileType } from "../../utils/appUtils";
+
+const SingleFileUploader = ({
+ label = "Upload Document",
+ required = false,
+ value,
+ onChange,
+ onRemove,
+ disabled,
+ error,
+ accept = ".pdf,.jpg,.jpeg,.png",
+ maxSizeMB = 25,
+ hint = "(PDF, JPG, PNG, max 5MB)",
+}) => {
+ const inputRef = useRef(null);
+
+ const handleFileSelect = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // Validate size
+ if (file.size > maxSizeMB * 1024 * 1024) {
+ alert(`File size cannot exceed ${maxSizeMB}MB`);
+ e.target.value = "";
+ return;
+ }
+
+ // Convert to base64
+ const base64Data = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => resolve(reader.result.split(",")[1]);
+ reader.onerror = (err) => reject(err);
+ });
+
+ const attachmentObj = {
+ fileName: file.name,
+ base64Data,
+ invoiceAttachmentTypeId: "", // set dynamically if needed
+ contentType: file.type,
+ fileSize: file.size,
+ description: "",
+ isActive: true,
+ };
+
+ onChange(attachmentObj);
+ e.target.value = "";
+ };
+
+ return (
+
+
+
+
inputRef.current.click()}
+ >
+
+
+ Click to select or click here to browse
+
+ {hint}
+
+
+
+
+ {error &&
{error}}
+
+ {value && (
+
+
+
+
+
+
+
+ {value.fileName}
+
+
+ {value.fileSize ? formatFileSize(value.fileSize) : ""}
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default SingleFileUploader;
diff --git a/src/components/purchase/DeliverChallanList.jsx b/src/components/purchase/DeliverChallanList.jsx
new file mode 100644
index 00000000..45ecf033
--- /dev/null
+++ b/src/components/purchase/DeliverChallanList.jsx
@@ -0,0 +1,68 @@
+import React from "react";
+import { useDeliverChallane } from "../../hooks/usePurchase";
+import { SpinnerLoader } from "../common/Loader";
+import { formatUTCToLocalTime } from "../../utils/dateUtils";
+import { FileView } from "../Expenses/Filelist";
+
+const DeliverChallanList = ({ purchaseId }) => {
+ const { data, isLoading, isError, error } = useDeliverChallane(purchaseId);
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+
+
+ );
+ }
+ if (!isLoading && data.length === 0)
+ return (
+
+ );
+
+ return (
+
+ {data.map((item) => (
+
+ {/* LEFT SIDE */}
+
+
+
{item.deliveryChallanNumber}
+
+ {formatUTCToLocalTime(item.deliveryChallanDate, true)}
+
+
+
+
+ Invoice:
+ {item.purchaseInvoice?.title} (
+ {item.purchaseInvoice?.purchaseInvoiceUId})
+
+
+
+ Description:{" "}
+ {item.description || "-"}
+
+
+ {item.attachment?.preSignedUrl && (
+
+ )}
+
+
+ ))}
+
+ );
+};
+
+export default DeliverChallanList;
diff --git a/src/components/purchase/DeliveryChallane.jsx b/src/components/purchase/DeliveryChallane.jsx
new file mode 100644
index 00000000..834f049d
--- /dev/null
+++ b/src/components/purchase/DeliveryChallane.jsx
@@ -0,0 +1,200 @@
+import React, { useState } from "react";
+import {
+ useAddDeliverChallan,
+ useDeliverChallane,
+} from "../../hooks/usePurchase";
+import { SpinnerLoader } from "../common/Loader";
+import { formatUTCToLocalTime } from "../../utils/dateUtils";
+import DeliverChallanList from "./DeliverChallanList";
+import { AppFormController, useAppForm } from "../../hooks/appHooks/useAppForm";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ DeliveryChallanDefaultValue,
+ DeliveryChallanSchema,
+} from "./PurchaseSchema";
+import Label from "../common/Label";
+import DatePicker from "../common/DatePicker";
+import { useInvoiceAttachmentTypes } from "../../hooks/masterHook/useMaster";
+import SelectField from "../common/Forms/SelectField";
+import Filelist from "../Expenses/Filelist";
+import SingleFileUploader from "../common/SigleFileUploader";
+import { localToUtc } from "../../utils/appUtils";
+import WarningBlock from "../InfoBlock/WarningBlock";
+import { FILE_UPLOAD_INFO } from "../../utils/staticContent";
+
+const DeliveryChallane = ({ purchaseId }) => {
+ const [file, setFile] = useState(null);
+
+ const {
+ register,
+ control,
+ handleSubmit,
+ setValue,
+ trigger,
+ watch,
+ reset,
+ formState: { errors },
+ } = useAppForm({
+ resolver: zodResolver(DeliveryChallanSchema),
+ defaultValues: DeliveryChallanDefaultValue,
+ });
+
+ const { data, isLoading } = useInvoiceAttachmentTypes();
+ const { mutate: AddChallan, isPending } = useAddDeliverChallan(() => {
+ setFile(null);
+ setValue("attachment", null, { shouldValidate: false });
+ reset({
+ ...DeliveryChallanDefaultValue,
+ attachment: null,
+ });
+ });
+ const onSubmit = async (formData) => {
+ const valid = await trigger();
+ if (!valid) return;
+ const { invoiceAttachmentTypeId, ...rest } = formData;
+
+ const payload = {
+ ...rest,
+ attachment: formData.attachment
+ ? {
+ ...formData.attachment,
+ invoiceAttachmentTypeId,
+ }
+ : null,
+ purchaseInvoiceId: purchaseId,
+ deliveryChallanDate: formData.deliveryChallanDate
+ ? localToUtc(formData.deliveryChallanDate)
+ : null,
+ };
+
+ AddChallan(payload);
+ };
+ const isUploaded = watch("attachment");
+ const isDocumentType = watch("invoiceAttachmentTypeId");
+ return (
+
+
Delivery Challans
+
+
+
+ {!isUploaded && (
+
+ )}
+
+
+
+
+ );
+};
+
+export default DeliveryChallane;
diff --git a/src/components/purchase/ManagePurchase.jsx b/src/components/purchase/ManagePurchase.jsx
new file mode 100644
index 00000000..fb757b54
--- /dev/null
+++ b/src/components/purchase/ManagePurchase.jsx
@@ -0,0 +1,238 @@
+import React, { useEffect, useMemo, useCallback, useState } from "react";
+import { AppFormProvider, useAppForm } from "../../hooks/appHooks/useAppForm";
+import { zodResolver } from "@hookform/resolvers/zod";
+
+import {
+ defaultPurchaseValue,
+ PurchaseSchema,
+ getStepFields,
+} from "./PurchaseSchema";
+
+import PurchasePartyDetails from "./PurchasePartyDetails";
+import PurchaseTransportDetails from "./PurchaseTransportDetails";
+import PurchasePaymentDetails from "./PurchasePaymentDetails";
+
+import {
+ useCreatePurchaseInvoice,
+ usePurchase,
+ useUpdatePurchaseInvoice,
+} from "../../hooks/usePurchase";
+import { error } from "pdf-lib";
+
+const ManagePurchase = ({ onClose, purchaseId }) => {
+ const { data } = usePurchase(purchaseId);
+
+ const [activeTab, setActiveTab] = useState(0);
+ const [completedTabs, setCompletedTabs] = useState([]);
+
+ const stepsConfig = useMemo(
+ () => [
+ {
+ name: "Vendor & Project Details",
+ icon: "bx bx-user bx-md",
+ subtitle: "Vendor information and project association",
+ component:
,
+ },
+ {
+ name: "Invoice & Logistics",
+ icon: "bx bx-receipt bx-md",
+ subtitle: "Invoice, e-Way bill, and logistics information",
+ component:
,
+ },
+ {
+ name: "Invoice & Tax Amount",
+ icon: "bx bx-credit-card bx-md",
+ subtitle: "Payment terms, tax breakdown, and due dates",
+ component:
,
+ },
+ ],
+ [data, purchaseId]
+ );
+
+ const purchaseOrder = useAppForm({
+ resolver: zodResolver(PurchaseSchema),
+ defaultValues: defaultPurchaseValue,
+ mode: "onChange",
+ shouldUnregister: false,
+ });
+
+ const { reset, formState } = purchaseOrder;
+
+ useEffect(() => {
+ if (!purchaseId || !data) return;
+
+ reset({
+ ...data,
+ projectId: data?.project?.id,
+ organizationId: data?.organization?.id,
+ supplierId: data?.supplier?.id,
+ invoiceAttachmentTypeId: null,
+ attachments:
+ data?.attachments?.map((doc) => ({
+ fileName: doc.fileName,
+ base64Data: null,
+ contentType: doc.contentType,
+ documentId: doc.documentId ?? null,
+ invoiceAttachmentTypeId: doc.invoiceAttachmentType?.id ?? null,
+ fileSize: 0,
+ description: "",
+ preSignedUrl: doc.preSignedUrl,
+ isActive: doc.isActive ?? true,
+ })) || [],
+ });
+
+ setCompletedTabs([0, 1, 2]);
+ }, [purchaseId, data, reset]);
+
+ const handleNext = useCallback(async () => {
+ const fields = getStepFields(activeTab);
+ const valid = await purchaseOrder.trigger(fields);
+
+ if (!valid) return;
+
+ setCompletedTabs((prev) => [...new Set([...prev, activeTab])]);
+ setActiveTab((prev) => Math.min(prev + 1, stepsConfig.length - 1));
+ }, [activeTab, purchaseOrder, stepsConfig.length]);
+
+ const handlePrev = useCallback(() => {
+ setActiveTab((prev) => Math.max(prev - 1, 0));
+ }, []);
+
+ const generatePatchOps = useCallback(
+ (formData) => {
+ const { dirtyFields } = formState;
+
+ return Object.keys(dirtyFields)
+ .filter((key) => key !== "invoiceAttachmentTypeId")
+ .map((key) => ({
+ operationType: 0,
+ path: `/${key}`,
+ op: "replace",
+ value: formData[key],
+ }));
+ },
+ [formState]
+ );
+
+ const { mutate: CreateInvoice, isPending } = useCreatePurchaseInvoice(() => {
+ reset();
+ onClose();
+ });
+
+ const { mutate: updatePurchase, isPending: isUpdating } =
+ useUpdatePurchaseInvoice(() => {
+ reset();
+ onClose();
+ });
+
+ const onSubmit = useCallback(
+ (formData) => {
+ if (purchaseId) {
+ const payload = generatePatchOps(formData);
+ updatePurchase({ purchaseId, payload });
+ } else {
+ CreateInvoice(formData);
+ }
+ },
+ [purchaseId, generatePatchOps, updatePurchase, CreateInvoice]
+ );
+ return (
+
+ {/* --- Steps Header --- */}
+
+ {stepsConfig.map((step, index) => {
+ const isActive = activeTab === index;
+ const isCompleted = completedTabs.includes(index);
+
+ return (
+
+
+
+
+
+ {index < stepsConfig.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+
+ {/* --- Form Content --- */}
+
+
+ {activeTab !== 2 && (
+
+ {stepsConfig[activeTab].component}
+
+
+
+
+
+
+
+ )}
+ {activeTab === 2 && (
+
+ )}
+
+
+
+ );
+};
+
+export default ManagePurchase;
diff --git a/src/components/purchase/PurchaseActions.jsx b/src/components/purchase/PurchaseActions.jsx
new file mode 100644
index 00000000..6cb82e66
--- /dev/null
+++ b/src/components/purchase/PurchaseActions.jsx
@@ -0,0 +1,102 @@
+export const getPurchaseActions = ({
+ item,
+ isActive,
+ canDelete,
+ canAddChallan,
+ setViewPurchase,
+ setManagePurchase,
+ setDeletingId,
+ setIsDeleteModalOpen,
+ setChallan,
+ setAddPayment,
+}) => {
+ const actions = [];
+
+ // VIEW
+ actions.push({
+ key: "view",
+ label: "View",
+ icon: "bx bx-show",
+ show: true,
+ onClick: () =>
+ setViewPurchase({
+ isOpen: true,
+ purchaseId: item.id,
+ }),
+ });
+
+ if (!isActive) {
+ // EDIT
+ actions.push({
+ key: "edit",
+ label: "Edit",
+ icon: "bx bx-edit",
+ show: true,
+ onClick: () =>
+ setManagePurchase({
+ isOpen: true,
+ purchaseId: item.id,
+ }),
+ });
+
+ // DELETE
+ actions.push({
+ key: "delete",
+ label: "Delete",
+ icon: "bx bx-trash",
+ show: canDelete,
+ onClick: () => {
+ setDeletingId(item.id);
+ setIsDeleteModalOpen(true);
+ },
+ });
+
+ // ADD CHALLAN
+ actions.push({
+ key: "challan",
+ label: "Add Delivery Challan",
+ icon: "bx bx-file bx-plus",
+ show: canAddChallan,
+ onClick: () =>
+ setChallan({
+ isOpen: true,
+ purchaseId: item.id,
+ }),
+ });
+
+ // ADD PAYMENT
+ actions.push({
+ key: "payment",
+ label: "Add Payment",
+ icon: "bx bx-wallet",
+ show: true,
+ onClick: () =>
+ setAddPayment({
+ isOpen: true,
+ purchaseId: item.id,
+ }),
+ });
+ } else {
+ // RESTORE
+ actions.push({
+ key: "restore",
+ label: "Restore",
+ icon: "bx bx-undo",
+ show: true,
+ onClick: () => {
+ setDeletingId(item.id);
+ setIsDeleteModalOpen(true);
+ },
+ });
+ }
+
+ return actions.filter((a) => a.show);
+};
+
+export const DropdownItem = ({ icon, label, onClick }) => (
+
+
+ {label}
+
+
+);
diff --git a/src/components/purchase/PurchaseList.jsx b/src/components/purchase/PurchaseList.jsx
new file mode 100644
index 00000000..5d7715b1
--- /dev/null
+++ b/src/components/purchase/PurchaseList.jsx
@@ -0,0 +1,188 @@
+import React, { useState } from "react";
+import {
+ useDeletePurchaseInvoice,
+ usePurchasesList,
+} from "../../hooks/usePurchase";
+import {
+ ADD_DELIVERY_CHALLAN,
+ DELETEPURCHASE_INVOICE,
+ ITEMS_PER_PAGE,
+} from "../../utils/constants";
+import Pagination from "../common/Pagination";
+import { PurchaseColumn } from "./Purchasetable";
+import { SpinnerLoader } from "../common/Loader";
+import { useDebounce } from "../../utils/appUtils";
+import { usePurchaseContext } from "../../pages/purchase/PurchasePage";
+import ConfirmModal from "../common/ConfirmModal";
+import { useHasUserPermission } from "../../hooks/useHasUserPermission";
+import { DropdownItem, getPurchaseActions } from "./PurchaseActions";
+
+const PurchaseList = ({ searchString, isActive }) => {
+ const { setViewPurchase, setManagePurchase, setChallan, setAddPayment } =
+ usePurchaseContext();
+ const [currentPage, setCurrentPage] = useState(1);
+ const { mutate: DeletePurchaseInvoice, isPending } =
+ useDeletePurchaseInvoice();
+ const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [deletingId, setDeletingId] = useState(null);
+
+ const canAddChallan = useHasUserPermission(ADD_DELIVERY_CHALLAN);
+ const canDelete = useHasUserPermission(DELETEPURCHASE_INVOICE);
+
+ const debounceSearch = useDebounce(searchString, 300);
+ const { data, isLoading } = usePurchasesList(
+ ITEMS_PER_PAGE,
+ currentPage,
+ // true,
+ !isActive,
+ {},
+ debounceSearch
+ );
+
+ const paginate = (page) => {
+ if (page >= 1 && page <= (data?.totalPages ?? 1)) {
+ setCurrentPage(page);
+ }
+ };
+
+ const visibleColumns = PurchaseColumn.filter((col) => !col.hidden);
+
+ const handleDeleteRestore = (id) => {
+ DeletePurchaseInvoice(
+ { id, isActive: isActive }, // delete if active, restore if deleted
+ {
+ onSettled: () => {
+ setDeletingId(null);
+ setIsDeleteModalOpen(false);
+ },
+ }
+ );
+ };
+
+ return (
+ <>
+ {IsDeleteModalOpen && (
+
setIsDeleteModalOpen(false)}
+ loading={isPending}
+ paramData={deletingId}
+ />
+ )}
+
+
+
+
+
+ {visibleColumns.map((col) => (
+ |
+ {col.label}
+ |
+ ))}
+ Action |
+
+
+
+
+ {/* LOADING */}
+ {isLoading && (
+
+ |
+
+
+
+ |
+
+ )}
+
+ {!isLoading && data?.data?.length === 0 && (
+
+ |
+ No Data Found
+ |
+
+ )}
+
+ {!isLoading &&
+ data?.data?.map((item, index) => (
+
+ {visibleColumns.map((col) => (
+ |
+ {col.render ? col.render(item) : item[col.key] || "NA"}
+ |
+ ))}
+
+
+
+
+ {getPurchaseActions({
+ item,
+ isActive,
+ canDelete,
+ canAddChallan,
+ setViewPurchase,
+ setManagePurchase,
+ setDeletingId,
+ setIsDeleteModalOpen,
+ setChallan,
+ setAddPayment,
+ }).map((action) => (
+
+ ))}
+
+
+ |
+
+ ))}
+
+
+
+
+ {data?.data?.length > 0 && (
+
+ )}
+
+ >
+ );
+};
+
+export default PurchaseList;
diff --git a/src/components/purchase/PurchasePartyDetails.jsx b/src/components/purchase/PurchasePartyDetails.jsx
new file mode 100644
index 00000000..221c33e9
--- /dev/null
+++ b/src/components/purchase/PurchasePartyDetails.jsx
@@ -0,0 +1,231 @@
+import React from "react";
+import {
+ AppFormController,
+ useAppFormContext,
+} from "../../hooks/appHooks/useAppForm";
+import Label from "../common/Label";
+import DatePicker from "../common/DatePicker";
+import {
+ SelectFieldSearch,
+ SelectProjectField,
+} from "../common/Forms/SelectFieldServerSide";
+import {
+ useGlobaleOrganizations,
+ useOrganization,
+ useOrganizationsList,
+} from "../../hooks/useOrganization";
+import { ITEMS_PER_PAGE } from "../../utils/constants";
+
+const PurchasePartyDetails = () => {
+ const {
+ register,
+ control,
+ setValue,
+ watch,
+ formState: { errors },
+ } = useAppFormContext();
+
+ return (
+
+ {/* Title */}
+
+
+
+
+
+ {errors?.title && (
+
{errors.title.message}
+ )}
+
+
+ {/* Project ID */}
+
+
+ setValue("projectId", val, {
+ shouldDirty: true,
+ shouldValidate: true,
+ })
+ }
+ errors={errors}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {errors?.billingAddress && (
+
{errors.billingAddress.message}
+ )}
+
+
+
+
+
+
+
+ {errors?.shippingAddress && (
+
{errors.shippingAddress.message}
+ )}
+
+
+ {/* Purchase Order Number */}
+
+
+
+
+
+ {errors?.purchaseOrderNumber && (
+
+ {errors.purchaseOrderNumber.message}
+
+ )}
+
+
+ {/* Purchase Order Date */}
+
+
+
+
+
+ {errors?.purchaseOrderDate && (
+
{errors.purchaseOrderDate.message}
+ )}
+
+
+ {/* Proforma Invoice */}
+
+
+
+
+ {errors?.proformaInvoiceNumber && (
+
+ {errors.proformaInvoiceNumber.message}
+
+ )}
+
+
+
+
+
+ {errors?.proformaInvoiceAmountt && (
+
+ {errors.proformaInvoiceAmount.message}
+
+ )}
+
+
+
+
+
+
+ {errors?.proformaInvoiceDate && (
+
+ {errors.proformaInvoiceDate.message}
+
+ )}
+
+
+ );
+};
+
+export default PurchasePartyDetails;
diff --git a/src/components/purchase/PurchasePayment.jsx b/src/components/purchase/PurchasePayment.jsx
new file mode 100644
index 00000000..530110a0
--- /dev/null
+++ b/src/components/purchase/PurchasePayment.jsx
@@ -0,0 +1,298 @@
+import React from "react";
+import {
+ AppFormController,
+ AppFormProvider,
+ useAppForm,
+} from "../../hooks/appHooks/useAppForm";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { AddPurchasePayment, defaultPurchasePayment } from "./PurchaseSchema";
+import { usePaymentAjustmentHead } from "../../hooks/masterHook/useMaster";
+import { formatFigure, localToUtc } from "../../utils/appUtils";
+import Label from "../common/Label";
+import DatePicker from "../common/DatePicker";
+import {
+ useAddPurchasePayment,
+ usePurchase,
+ usePurchasePaymentHistory,
+} from "../../hooks/usePurchase";
+import SelectField from "../common/Forms/SelectField";
+import { SpinnerLoader } from "../common/Loader";
+import { formatUTCToLocalTime } from "../../utils/dateUtils";
+import Avatar from "../common/Avatar";
+
+const PurchasePayment = ({onClose, purchaseId }) => {
+ const {
+ data: Purchase,
+ isLoading: isPurchaseLoading,
+ error: purchaseError,
+ } = usePurchase(purchaseId);
+ const methods = useAppForm({
+ resolver: zodResolver(AddPurchasePayment),
+ defaultValues: defaultPurchasePayment,
+ });
+ const {
+ control,
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors },
+ } = methods;
+
+ const {
+ data: paymentTypes,
+ isLoading: isPaymentTypeLoading,
+ isError: isPaymentTypeError,
+ error: paymentError,
+ } = usePaymentAjustmentHead(true);
+
+ const { mutate: AddPayment, isPending } = useAddPurchasePayment(() => {
+ handleClose();
+ });
+ const { data, isLoading, isError, error } =
+ usePurchasePaymentHistory(purchaseId);
+ const onSubmit = (formData) => {
+ const payload = {
+ ...formData,
+ paymentReceivedDate: localToUtc(formData.paymentReceivedDate),
+ invoiceId: purchaseId,
+ };
+ AddPayment(payload);
+ };
+
+ const handleClose = (formData) => {
+ reset(defaultPurchasePayment);
+ };
+ return (
+
+
+
Supplier / Vendor Transaction
+
+
+
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ data?.length > 0 && (
+
+ {data
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
+ .map((payment, index) => (
+
+
+
+
+
+
+ Transaction Date:
+ {" "}
+ {formatUTCToLocalTime(
+ payment.paymentReceivedDate
+ )}
+
+
+ {formatFigure(payment.amount, {
+ type: "currency",
+ currency: "INR",
+ })}
+
+
+
+
+
+ Updated By:
+ {" "}
+
{" "}
+ {payment?.createdBy?.firstName}{" "}
+ {payment.createdBy?.lastName}
+
+
+
+
+
+
+
+ Transaction ID:
+ {" "}
+ {payment.transactionId}
+
+
+
+
+
+ {payment?.paymentAdjustmentHead?.name}
+
+
+ {formatFigure(payment.amount, {
+ type: "currency",
+ currency: "INR",
+ })}
+
+
+
+ {payment?.comment}
+
+
+
+
+
+ ))}
+
+ )
+ )}
+ {data?.length === 0 && (
+
+
+
+
+
You don't have any payment yet.
+
+
+ )}
+
+
+
+ );
+};
+
+export default PurchasePayment;
diff --git a/src/components/purchase/PurchasePaymentDetails.jsx b/src/components/purchase/PurchasePaymentDetails.jsx
new file mode 100644
index 00000000..42d526c8
--- /dev/null
+++ b/src/components/purchase/PurchasePaymentDetails.jsx
@@ -0,0 +1,299 @@
+import React, { useEffect } from "react";
+import Label from "../common/Label";
+import {
+ AppFormController,
+ useAppFormContext,
+} from "../../hooks/appHooks/useAppForm";
+import DatePicker from "../common/DatePicker";
+import { useInvoiceAttachmentTypes } from "../../hooks/masterHook/useMaster";
+import Filelist from "../Expenses/Filelist";
+import SelectField from "../common/Forms/SelectField";
+import { useWatch } from "react-hook-form";
+import WarningBlock from "../InfoBlock/WarningBlock";
+import { FILE_UPLOAD_INFO } from "../../utils/staticContent";
+
+const PurchasePaymentDetails = ({ purchaseId = null }) => {
+ const { data, isLoading, error, isError } = useInvoiceAttachmentTypes();
+ const { data: InvoiceDocTypes, isLoading: invoiceDocLoading } =
+ useInvoiceAttachmentTypes();
+ const {
+ register,
+ watch,
+ setValue,
+ control,
+ formState: { errors },
+ } = useAppFormContext();
+
+ const baseAmount = watch("baseAmount");
+ const taxAmount = watch("taxAmount");
+ const trCharge = watch("transportCharges");
+ useEffect(() => {
+ const base = parseFloat(baseAmount) || 0;
+ const tax = parseFloat(taxAmount) || 0;
+ const transportCharges = parseFloat(trCharge) || 0;
+
+ if (base || tax || transportCharges) {
+ setValue("totalAmount", (base + tax + transportCharges).toFixed(2));
+ }
+ }, [baseAmount, taxAmount, trCharge, setValue]);
+ const invoiceAttachmentType = watch("invoiceAttachmentTypeId");
+ const files = watch("attachments");
+ 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 onFileChange = async (e) => {
+ const newFiles = Array.from(e.target.files);
+ if (newFiles.length === 0) return;
+
+ const existingFiles = watch("attachments") || [];
+
+ 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,
+ invoiceAttachmentTypeId: invoiceAttachmentType,
+ isNew: true, // <-- temp
+ tempId: crypto.randomUUID(), // temp - id
+ };
+ })
+ );
+
+ const combinedFiles = [
+ ...existingFiles,
+ ...parsedFiles.filter(
+ (newFile) =>
+ !existingFiles.some(
+ (f) =>
+ f.fileName === newFile.fileName && f.fileSize === newFile.fileSize
+ )
+ ),
+ ];
+
+ setValue("attachments", combinedFiles, {
+ shouldDirty: true,
+ shouldValidate: true,
+ });
+ };
+
+ const removeFile = (index) => {
+ const updated = files.flatMap((file, i) => {
+ // NEW FILE → remove completely
+ if (file.isNew && file.tempId === index) {
+ return []; // remove from array
+ }
+
+ // EXISTING FILE → mark inactive
+ if (!file.isNew && file.documentId === index) {
+ return [{ ...file, isActive: false }];
+ }
+
+ return [file];
+ });
+
+ setValue("attachments", updated, {
+ shouldDirty: true,
+ shouldValidate: true,
+ });
+ };
+
+ return (
+
+
+
+
+
+
+ {errors?.baseAmount && (
+
+ {errors.baseAmount.message}
+
+ )}
+
+
+
+
+
+
+
+ {errors?.taxAmount && (
+
+ {errors.taxAmount.message}
+
+ )}
+
+
+
+
+
+
+ {errors?.transportCharges && (
+
+ {errors.transportCharges.message}
+
+ )}
+
+
+
+
+
+
+
+ {errors?.totalAmount && (
+
+ {errors.totalAmount.message}
+
+ )}
+
+
+
+
+
+
+
+ {errors?.paymentDueDate && (
+
+ {errors.paymentDueDate.message}
+
+ )}
+
+
+
+
+
+
+
+ {errors?.description && (
+
+ {errors.description.message}
+
+ )}
+
+
+
(
+
+ )}
+ />
+
+ {errors?.invoiceAttachmentTypeId && (
+
+ {errors.invoiceAttachmentTypeId.message}
+
+ )}
+
+
+
+
+
+
+
document.getElementById("attachments").click()}
+ >
+
+
+ Click to select or click here to browse
+
+ (PDF, JPG, PNG, max 5MB)
+
+ {
+ onFileChange(e);
+ e.target.value = "";
+ }}
+ />
+
+ {errors.attachments && (
+
{errors.attachments.message}
+ )}
+ {files.length > 0 && (
+
+ )}
+
+ {Array.isArray(errors.attachments) &&
+ errors.attachments.map((fileError, index) => (
+
+ {
+ (fileError?.fileSize?.message ||
+ fileError?.contentType?.message ||
+ fileError?.base64Data?.message,
+ fileError?.documentId?.message)
+ }
+
+ ))}
+ {!invoiceAttachmentType && (
+
+ )}
+
+
+
+ );
+};
+
+export default PurchasePaymentDetails;
diff --git a/src/components/purchase/PurchaseSchema.jsx b/src/components/purchase/PurchaseSchema.jsx
new file mode 100644
index 00000000..adb9258a
--- /dev/null
+++ b/src/components/purchase/PurchaseSchema.jsx
@@ -0,0 +1,211 @@
+import { z } from "zod";
+import { normalizeAllowedContentTypes } from "../../utils/appUtils";
+
+const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+const ALLOWED_TYPES = [
+ "application/pdf",
+ "image/png",
+ "image/jpg",
+ "image/jpeg",
+];
+
+export const AttachmentSchema = z.object({
+ invoiceAttachmentTypeId: z.string().nullable(),
+ 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",
+ }),
+ fileSize: z.number().max(MAX_FILE_SIZE, {
+ message: "File size must be less than or equal to 5MB",
+ }),
+ description: z.string().optional().default(""),
+ isActive: z.boolean().default(true),
+ documentId: z.string().nullable().default(null),
+});
+
+export const PurchaseSchema = z.object({
+ title: z.string().min(1, { message: "Title is required" }),
+ projectId: z.string().min(1, { message: "Project is required" }),
+ organizationId: z.string().min(1, { message: "Organization is required" }),
+ billingAddress: z.string().min(1, { message: "Address is required" }),
+ shippingAddress: z.string().min(1, { message: "Address is required" }),
+
+ purchaseOrderNumber: z.string().nullable(),
+ purchaseOrderDate: z.coerce.date().nullable(),
+
+ supplierId: z.string().min(1, { message: "Supplier is required" }),
+
+ proformaInvoiceNumber: z.string().nullable(),
+ proformaInvoiceDate: z.coerce.date().nullable(),
+ proformaInvoiceAmount: z.coerce.number().optional(),
+
+ invoiceNumber: z.string().nullable(),
+ invoiceDate: z.coerce.date().nullable(),
+ eWayBillNumber: z.string().nullable(),
+ eWayBillDate: z.coerce.date().nullable(),
+ invoiceReferenceNumber: z.string().nullable(),
+ acknowledgmentDate: z.coerce.date().nullable(),
+ acknowledgmentNumber: z.string().nullable(),
+
+ baseAmount: z
+ .number()
+ .or(z.nan()) // allow NaN
+ .transform((val) => (Number.isNaN(val) ? null : val)),
+
+ taxAmount: z
+ .number()
+ .or(z.nan()) // allow NaN
+ .transform((val) => (Number.isNaN(val) ? null : val)),
+ totalAmount: z
+ .number()
+ .or(z.nan()) // allow NaN
+ .transform((val) => (Number.isNaN(val) ? null : val)),
+ paymentDueDate: z.coerce.date().nullable(),
+ transportCharges: z
+ .number()
+ .or(z.nan()) // allow NaN
+ .transform((val) => (Number.isNaN(val) ? null : val)),
+ description: z.string(),
+ invoiceAttachmentTypeId: z.string().nullable(),
+ attachments: z.array(AttachmentSchema),
+});
+
+export const defaultPurchaseValue = {
+ title: "",
+ projectId: "",
+ organizationId: "",
+ billingAddress: "",
+ shippingAddress: "",
+ purchaseOrderNumber: null,
+ purchaseOrderDate: null,
+
+ supplierId: "",
+
+ proformaInvoiceNumber: null,
+ proformaInvoiceDate: null,
+ proformaInvoiceAmount: 0,
+
+ invoiceNumber: null,
+ invoiceDate: null,
+ eWayBillNumber: null,
+ eWayBillDate: null,
+ invoiceReferenceNumber: null,
+ acknowledgmentNumber: null,
+ acknowledgmentDate: null,
+
+ baseAmount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ paymentDueDate: null,
+ transportCharges: 0,
+ description: "",
+ invoiceAttachmentTypeId: null,
+ attachments: [],
+};
+
+export const getStepFields = (stepIndex) => {
+ const stepFieldMap = {
+ 0: [
+ "title",
+ "projectId",
+ "organizationId",
+ "supplierId",
+ "billingAddress",
+ "shippingAddress",
+ "purchaseOrderNumber",
+ "purchaseOrderDate",
+ "proformaInvoiceNumber",
+ "proformaInvoiceDate",
+ "proformaInvoiceAmount",
+ ],
+ 1: [
+ "invoiceNumber",
+ "invoiceDate",
+ "eWayBillNumber",
+ "eWayBillDate",
+ "invoiceReferenceNumber",
+ "acknowledgmentNumber",
+ "acknowledgmentDate",
+ ],
+ 2: [
+ "baseAmount",
+ "taxAmount",
+ "totalAmount",
+ "transportCharges",
+ "paymentDueDate",
+ "invoiceAttachmentTypeId",
+ "description",
+ "attachments",
+ ],
+ };
+
+ return stepFieldMap[stepIndex] || [];
+};
+
+export const SingleAttachmentSchema = z.object({
+ fileName: z.string().min(1, { message: "File name is required" }),
+ base64Data: z.string().min(1, { message: "File data is required" }),
+ invoiceAttachmentTypeId: z.string().nullable(),
+
+ contentType: z
+ .string()
+ .min(1, { message: "MIME type is required" })
+ .refine(
+ (val) =>
+ ["application/pdf", "image/jpeg", "image/jpg", "image/png"].includes(
+ val
+ ),
+ {
+ message: "File type must be PDF, JPG, JPEG or PNG",
+ }
+ ),
+
+ fileSize: z
+ .number()
+ .int()
+ .nonnegative("fileSize must be ≥ 0")
+ .max(5 * 1024 * 1024, "File size must be ≤ 5MB"),
+
+ description: z.string().optional().default(""),
+ isActive: z.boolean(),
+});
+
+export const DeliveryChallanSchema = z.object({
+ deliveryChallanNumber: z
+ .string()
+ .min(1, { message: "Challan Number required" }),
+ invoiceAttachmentTypeId: z.string().nullable(),
+ deliveryChallanDate: z.string().min(1, { message: "Deliver date required" }),
+ description: z.string().min(1, { message: "Description required" }),
+ attachment: z
+ .any()
+ .refine((val) => val && typeof val === "object" && !!val.base64Data, {
+ message: "Please upload document",
+ }),
+});
+
+export const DeliveryChallanDefaultValue = {
+ deliveryChallanNumber: "",
+ deliveryChallanDate: "",
+ description: "",
+ attachment: null,
+ invoiceAttachmentTypeId: null,
+};
+
+export const AddPurchasePayment = 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" }),
+});
+export const defaultPurchasePayment = {
+ paymentReceivedDate: null,
+ transactionId: "",
+ amount: 0,
+ comment: "",
+ paymentAdjustmentHeadId: "",
+};
diff --git a/src/components/purchase/PurchaseTransportDetails.jsx b/src/components/purchase/PurchaseTransportDetails.jsx
new file mode 100644
index 00000000..d64f0058
--- /dev/null
+++ b/src/components/purchase/PurchaseTransportDetails.jsx
@@ -0,0 +1,131 @@
+import React from "react";
+import { useAppFormContext } from "../../hooks/appHooks/useAppForm";
+import DatePicker from "../common/DatePicker";
+import Label from "../common/Label";
+
+const PurchaseTransportDetails = () => {
+ const {
+ register,control,
+ formState: { errors },
+ } = useAppFormContext();
+
+ return (
+
+ {/* Invoice Number */}
+
+
+
+
+
+ {errors?.invoiceNumber && (
+
+ {errors.invoiceNumber.message}
+
+ )}
+
+
+ {/* Invoice Date */}
+
+
+
+
+
+
+ {errors?.invoiceDate && (
+
+ {errors.invoiceDate.message}
+
+ )}
+
+
+ {/* E-Way Bill Number */}
+
+
+
+
+
+ {errors?.eWayBillNumber && (
+
+ {errors.eWayBillNumber.message}
+
+ )}
+
+
+ {/* E-Way Bill Date */}
+
+
+
+
+
+
+ {errors?.eWayBillDate && (
+
+ {errors.eWayBillDate.message}
+
+ )}
+
+
+ {/* Invoice Reference Number */}
+
+
+
+
+
+ {errors?.invoiceReferenceNumber && (
+
+ {errors.invoiceReferenceNumber.message}
+
+ )}
+
+
+ {/* Acknowledgment Number */}
+
+
+
+
+
+ {errors?.acknowledgmentNumber && (
+
+ {errors.acknowledgmentNumber.message}
+
+ )}
+
+
+
+
+
+
+
+ {errors?.acknowledgmentDate && (
+
+ {errors.acknowledgmentDate.message}
+
+ )}
+
+
+ );
+};
+
+export default PurchaseTransportDetails;
diff --git a/src/components/purchase/Purchasetable.jsx b/src/components/purchase/Purchasetable.jsx
new file mode 100644
index 00000000..614cc269
--- /dev/null
+++ b/src/components/purchase/Purchasetable.jsx
@@ -0,0 +1,60 @@
+import { formatFigure, getColorNameFromHex } from "../../utils/appUtils";
+
+export const PurchaseColumn = [
+ {
+ key: "purchaseInvoiceUId",
+ label: "Invoice Id",
+
+ className: "text-start",
+ render: (item) => (
+
+ {item?.purchaseInvoiceUId || "NA"}
+
+ ),
+ },
+ {
+ key: "title",
+ label: "Title",
+
+ className: "text-start",
+ render: (item) => (
+
+ {item?.title || "NA"}
+
+ ),
+ },
+ {
+ key: "project",
+ label: "Project",
+ className: "text-start ",
+ render: (item) => {item?.project?.name || "NA"},
+ },
+ {
+ key: "supplier",
+ label: "Supplier",
+ className: "text-start ",
+ render: (item) => {item?.supplier?.name || "NA"},
+ },
+ {
+ key: "status",
+ label: "Status",
+ className: "text-start",
+ render: (item) => {
+ let color = getColorNameFromHex(item.status?.color);
+ return (
+
+
+ {item?.status.displayName || "NA"}
+
+ );
+ },
+ },
+ {
+ key: "totalAmount",
+ label: "Total Amount",
+ className: "text-end",
+ render: (item) => (
+ {formatFigure(item?.totalAmount, { type: "currency" })}
+ ),
+ },
+];
diff --git a/src/components/purchase/ViewPurchase.jsx b/src/components/purchase/ViewPurchase.jsx
new file mode 100644
index 00000000..6cb07df8
--- /dev/null
+++ b/src/components/purchase/ViewPurchase.jsx
@@ -0,0 +1,243 @@
+import React from "react";
+import { usePurchase } from "../../hooks/usePurchase";
+import { SpinnerLoader } from "../common/Loader";
+import { formatUTCToLocalTime } from "../../utils/dateUtils";
+import { getColorNameFromHex } from "../../utils/appUtils";
+
+const ViewPurchase = ({ purchaseId }) => {
+ const { data, isLoading, isError, error } = usePurchase(purchaseId);
+
+ if (isLoading) return ;
+ if (isError) return ;
+
+ return (
+
+
+
Purchase Invoice Details
+
+
+
+
+
+
Purchase No:
+
{data?.purchaseInvoiceUId}
+
+
+
+
+
+ {data?.status?.displayName || "NA"}
+
+
+
+
+
+
+
+
+
Purchase Details
+
+
+
Title:
+
{data?.title}
+
+
+
Description:
+
{data?.description}
+
+
+
+
+
+
+
+
+ {/* Project Name */}
+
+
Project Name:
+
{data?.project?.name}
+
+
+ {/* Organization Name */}
+
+
Organization:
+
{data?.organization?.name}
+
+
+ {/* Email */}
+
+
Email:
+
{data?.organization?.email}
+
+
+ {/* Contact Number */}
+
+
Contact:
+
{data?.organization?.contactNumber}
+
+
+ {/* Address */}
+
+
Address:
+
{data?.organization?.address}
+
+
+
+
+
+
+
+
Supplier
+
+ {/* Supplier Name */}
+
+
Name:
+
{data?.supplier?.name}
+
+
+ {/* Contact Person */}
+
+
Contact Person:
+
{data?.supplier?.contactPerson}
+
+
+ {/* Email */}
+
+
Email:
+
{data?.supplier?.email}
+
+
+ {/* Contact Number */}
+
+
Contact:
+
{data?.supplier?.contactNumber}
+
+
+ {/* Address */}
+
+
Address:
+
{data?.supplier?.address}
+
+
+
+
+
+
+
Invoice Details
+
+
+ {/* Left column */}
+
+
+
Invoice No:
+
{data?.invoiceNumber}
+
+
+
+
Proforma No:
+
{data?.proformaInvoiceNumber}
+
+
+
+
E-Way Bill:
+
{data?.eWayBillNumber}
+
+
+
+
PO No:
+
{data?.purchaseOrderNumber}
+
+
+
+ {/* Right column */}
+
+
+
Invoice Date:
+
+ {formatUTCToLocalTime(data?.invoiceDate)}
+
+
+
+
+
Proforma Date:
+
+ {formatUTCToLocalTime(data?.proformaInvoiceDate)}
+
+
+
+
+
E-Way Date:
+
+ {formatUTCToLocalTime(data?.eWayBillDate)}
+
+
+
+
+
PO Date:
+
+ {formatUTCToLocalTime(data?.purchaseOrderDate)}
+
+
+
+
+
+
+
+
+
+
Amount Summary
+
+
+
Base Amount
+
₹ {data?.baseAmount}
+
+
+
+
Tax
+
₹ {data?.taxAmount}
+
+
+
+
Transport
+
₹ {data?.transportCharges}
+
+
+
+
+
+ Total
+ ₹ {data?.totalAmount}
+
+
+
+
Due Date:
+
+ {formatUTCToLocalTime(data?.paymentDueDate)}
+
+
+
+
+
+
+
+
+
Billing Address
+
+
{data?.billingAddress || "-"}
+
+
+
+
+
+
Shipping Address
+
{data?.shippingAddress || "-"}
+
+
+
+
+ );
+};
+
+export default ViewPurchase;
diff --git a/src/hooks/appHooks/useAppForm.js b/src/hooks/appHooks/useAppForm.js
index ea751205..f4ccbac0 100644
--- a/src/hooks/appHooks/useAppForm.js
+++ b/src/hooks/appHooks/useAppForm.js
@@ -1,6 +1,6 @@
-import { useForm, Controller,FormProvider } from "react-hook-form";
+import { useForm, Controller, FormProvider, useFormContext } from "react-hook-form";
export const useAppForm = (config) => useForm(config);
export const AppFormProvider = FormProvider;
export const AppFormController = Controller;
-
+export const useAppFormContext = useFormContext;
diff --git a/src/hooks/masterHook/useMaster.js b/src/hooks/masterHook/useMaster.js
index ca5de298..b4157b69 100644
--- a/src/hooks/masterHook/useMaster.js
+++ b/src/hooks/masterHook/useMaster.js
@@ -10,16 +10,25 @@ import {
} from "@tanstack/react-query";
import showToast from "../../services/toastService";
-
-export const useRecurringStatus = ()=>{
+export const useInvoiceAttachmentTypes = () => {
return useQuery({
- queryKey:["RecurringStatus"],
- queryFn:async()=>{
+ queryKey: ["invoiceAttachmentType"],
+ queryFn: async () => {
+ const resp = await MasterRespository.getInvoiceAttachmentTypes();
+ return resp.data;
+ },
+ });
+};
+
+export const useRecurringStatus = () => {
+ return useQuery({
+ queryKey: ["RecurringStatus"],
+ queryFn: async () => {
const resp = await MasterRespository.getRecurringStatus();
- return resp.data
- }
- })
-}
+ return resp.data;
+ },
+ });
+};
export const useCurrencies = () => {
return useQuery({
queryKey: ["currencies"],
@@ -31,10 +40,10 @@ export const useCurrencies = () => {
};
export const usePaymentAjustmentHead = (isActive) => {
- return useQuery({
- queryKey: ["paymentType",isActive],
- queryFn: async () => await MasterRespository.getPaymentAdjustmentHead(isActive),
-
+ return useQuery({
+ queryKey: ["paymentType", isActive],
+ queryFn: async () =>
+ await MasterRespository.getPaymentAdjustmentHead(isActive),
});
};
@@ -296,26 +305,26 @@ export const useOrganizationType = () => {
});
};
-export const useJobStatus=(statusId,projectId)=>{
+export const useJobStatus = (statusId, projectId) => {
return useQuery({
- queryKey:["Job_Staus",statusId,projectId],
- queryFn:async()=>{
- const resp = await MasterRespository.getJobStatus(statusId,projectId);
+ queryKey: ["Job_Staus", statusId, projectId],
+ queryFn: async () => {
+ const resp = await MasterRespository.getJobStatus(statusId, projectId);
return resp.data;
},
- enabled:!!statusId && !!projectId
- })
-}
+ enabled: !!statusId && !!projectId,
+ });
+};
-export const useTeamRole=()=>{
+export const useTeamRole = () => {
return useQuery({
- queryKey:["Team_Role"],
- queryFn:async()=>{
+ queryKey: ["Team_Role"],
+ queryFn: async () => {
const resp = await MasterRespository.getTeamRole();
return resp.data;
- }
- })
-}
+ },
+ });
+};
//#region ==Get Masters==
const fetchMasterData = async (masterType) => {
switch (masterType) {
@@ -405,8 +414,6 @@ const useMaster = () => {
export default useMaster;
//#endregion
-
-
// ================================Mutation====================================
//#region Job Role
@@ -456,10 +463,6 @@ export const useCreateJobRole = (onSuccessCallback) => {
};
//#endregion Job Role
-
-
-
-
//#region Application Role
export const useCreateApplicationRole = (onSuccessCallback) => {
const queryClient = useQueryClient();
@@ -505,10 +508,6 @@ export const useUpdateApplicationRole = (onSuccessCallback) => {
};
//#endregion
-
-
-
-
//#region Create work Category
export const useCreateWorkCategory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@@ -554,11 +553,6 @@ export const useUpdateWorkCategory = (onSuccessCallback) => {
};
//#endregion
-
-
-
-
-
//#region Contact Category
export const useCreateContactCategory = (onSuccessCallback) => {
@@ -609,10 +603,6 @@ export const useUpdateContactCategory = (onSuccessCallback) => {
//#endregion
-
-
-
-
//#region Contact Tag
export const useCreateContactTag = (onSuccessCallback) => {
@@ -659,10 +649,6 @@ export const useUpdateContactTag = (onSuccessCallback) => {
};
//#endregion
-
-
-
-
//#region Expense Category
export const useCreateExpenseCategory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@@ -689,7 +675,10 @@ export const useUpdateExpenseCategory = (onSuccessCallback) => {
return useMutation({
mutationFn: async ({ id, payload }) => {
- const response = await MasterRespository.updateExpenseCategory(id, payload);
+ const response = await MasterRespository.updateExpenseCategory(
+ id,
+ payload
+ );
return response.data;
},
onSuccess: (data, variables) => {
@@ -708,11 +697,6 @@ export const useUpdateExpenseCategory = (onSuccessCallback) => {
//#endregion
-
-
-
-
-
//#region Payment Mode
export const useCreatePaymentMode = (onSuccessCallback) => {
@@ -759,10 +743,6 @@ export const useUpdatePaymentMode = (onSuccessCallback) => {
//#endregion
-
-
-
-
// Services-------------------------------
// export const useCreateService = (onSuccessCallback) => {
@@ -844,10 +824,6 @@ export const useUpdateService = (onSuccessCallback) => {
//#endregion
-
-
-
-
//#region Activity Grouph
export const useCreateActivityGroup = (onSuccessCallback) => {
@@ -912,10 +888,6 @@ export const useUpdateActivityGroup = (onSuccessCallback) => {
//#endregion
-
-
-
-
//#region Activities
export const useCreateActivity = (onSuccessCallback) => {
const queryClient = useQueryClient();
@@ -970,11 +942,6 @@ export const useUpdateActivity = (onSuccessCallback) => {
//#endregion
-
-
-
-
-
//#region Expense Status
export const useCreateExpenseStatus = (onSuccessCallback) => {
const queryClient = useQueryClient();
@@ -1018,10 +985,6 @@ export const useUpdateExpenseStatus = (onSuccessCallback) => {
};
//#endregion
-
-
-
-
//#region Document-Category
export const useCreateDocumentCatgory = (onSuccessCallback) => {
const queryClient = useQueryClient();
@@ -1068,11 +1031,6 @@ export const useUpdateDocumentCategory = (onSuccessCallback) => {
};
//#endregion
-
-
-
-
-
//#region Document-Type
export const useCreateDocumentType = (onSuccessCallback) => {
const queryClient = useQueryClient();
@@ -1117,10 +1075,6 @@ export const useUpdateDocumentType = (onSuccessCallback) => {
};
//#endregion
-
-
-
-
//#region Payment Adjustment Head
export const useCreatePaymentAjustmentHead = (onSuccessCallback) => {
const queryClient = useQueryClient();
@@ -1147,7 +1101,10 @@ export const useUpdatePaymentAjustmentHead = (onSuccessCallback) => {
return useMutation({
mutationFn: async ({ id, payload }) => {
- const resp = await MasterRespository.updatePaymentAjustmentHead(id, payload);
+ const resp = await MasterRespository.updatePaymentAjustmentHead(
+ id,
+ payload
+ );
return resp.data;
},
onSuccess: (data) => {
@@ -1164,10 +1121,6 @@ export const useUpdatePaymentAjustmentHead = (onSuccessCallback) => {
};
//#endregion
-
-
-
-
//#region ==Delete Master==
export const useDeleteMasterItem = () => {
const queryClient = useQueryClient();
@@ -1203,8 +1156,6 @@ export const useDeleteMasterItem = () => {
//#endregion
-
-
export const useDeleteServiceGroup = () => {
const queryClient = useQueryClient();
diff --git a/src/hooks/useOrganization.js b/src/hooks/useOrganization.js
index 1da12f21..dde865fa 100644
--- a/src/hooks/useOrganization.js
+++ b/src/hooks/useOrganization.js
@@ -24,7 +24,9 @@ export const useOrganizationModal = () => {
dispatch(
openOrgModal({
isOpen: true,
- orgData: options.hasOwnProperty("orgData") ? options.orgData : orgData,
+ orgData: options.hasOwnProperty("orgData")
+ ? options.orgData
+ : orgData,
startStep: options.startStep ?? startStep ?? 1,
prevStep: options.prevStep ?? prevStep ?? 1,
flowType: options.flowType ?? flowType ?? "default",
@@ -37,16 +39,30 @@ export const useOrganizationModal = () => {
// ================================Query=============================================================
-export const useOrganization=(id)=>{
-return useQuery({
- queryKey:["organization",id],
- queryFn:async()=> {
- const resp = await await OrganizationRepository.getOrganizaion(id);
- return resp.data
- },
- enabled:!!id
-})
-}
+export const useGlobaleOrganizations = (pageSize, pageNumber,id, searchString) => {
+ return useQuery({
+ queryKey: ["global_organization",pageSize, pageNumber,id, searchString],
+ queryFn: async () => {
+ const resp = await OrganizationRepository.getGlobalOrganization(
+ pageSize,
+ pageNumber,id,
+ searchString
+ );
+ return resp.data;
+ },
+ });
+};
+
+export const useOrganization = (id) => {
+ return useQuery({
+ queryKey: ["organization", id],
+ queryFn: async () => {
+ const resp = await await OrganizationRepository.getOrganizaion(id);
+ return resp.data;
+ },
+ enabled: !!id,
+ });
+};
export const useOrganizationBySPRID = (sprid) => {
return useQuery({
queryKey: ["organization by", sprid],
@@ -101,7 +117,7 @@ export const useOrganizationEmployees = (
organizationId,
searchString
),
- enabled: !!projectId ,
+ enabled: !!projectId,
});
};
@@ -138,10 +154,10 @@ export const useAssignOrgToProject = (onSuccessCallback) => {
useClient.invalidateQueries({
queryKey: ["projectAssignedOrganiztions"],
});
- useClient.invalidateQueries({
+ useClient.invalidateQueries({
queryKey: ["projectAssignedOrganiztionsName"],
});
-
+
useClient.invalidateQueries({
queryKey: ["projectAssignedServices", projectId],
});
@@ -181,12 +197,14 @@ export const useAssignOrgToTenant = (onSuccessCallback) => {
export const useUpdateOrganization = (onSuccessCallback) => {
const useClient = useQueryClient();
return useMutation({
- mutationFn: async ({orgId,payload}) =>
- await OrganizationRepository.updateOrganizaion(orgId,payload),
+ mutationFn: async ({ orgId, payload }) =>
+ await OrganizationRepository.updateOrganizaion(orgId, payload),
onSuccess: (_, variables) => {
useClient.invalidateQueries({ queryKey: ["organizationList"] });
- useClient.invalidateQueries({ queryKey: ["projectAssignedOrganiztionsName"] });
-
+ useClient.invalidateQueries({
+ queryKey: ["projectAssignedOrganiztionsName"],
+ });
+
showToast("Organization Updated successfully", "success");
if (onSuccessCallback) onSuccessCallback();
},
diff --git a/src/hooks/useProjects.js b/src/hooks/useProjects.js
index 75467399..973d8b94 100644
--- a/src/hooks/useProjects.js
+++ b/src/hooks/useProjects.js
@@ -20,14 +20,15 @@ export const useCurrentService = () => {
// ------------------------------Query-------------------
-export const useProjects = (pageSize, pageNumber) => {
+export const useProjects = (pageSize, pageNumber,searchString) => {
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
return useQuery({
- queryKey: ["ProjectsList", pageSize, pageNumber],
+ queryKey: ["ProjectsList", pageSize, pageNumber,searchString],
queryFn: async () => {
const response = await ProjectRepository.getProjectList(
pageSize,
- pageNumber
+ pageNumber,
+ searchString,
);
return response?.data;
},
diff --git a/src/hooks/usePurchase.jsx b/src/hooks/usePurchase.jsx
new file mode 100644
index 00000000..97d85953
--- /dev/null
+++ b/src/hooks/usePurchase.jsx
@@ -0,0 +1,181 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { PurchaseRepository } from "../repositories/PurchaseRepository";
+import showToast from "../services/toastService";
+
+export const usePurchasesList = (
+ pageSize,
+ pageNumber,
+ isActive,
+ filter,
+ searchString
+) => {
+ return useQuery({
+ queryKey: [
+ "purchase_list",
+ pageSize,
+ pageNumber,
+ isActive,
+ filter,
+ searchString,
+ ],
+ queryFn: async () => {
+ const resp = await PurchaseRepository.GetPurchaseList(
+ pageSize,
+ pageNumber,
+ isActive,
+ filter,
+ searchString
+ );
+ return resp.data;
+ },
+ });
+};
+
+export const useDeliverChallane = (purchaseInvoiceId) => {
+ return useQuery({
+ queryKey: ["deliverliy_challane", purchaseInvoiceId],
+ queryFn: async () => {
+ const resp = await PurchaseRepository.GetDeliveryChallan(
+ purchaseInvoiceId
+ );
+ return resp.data;
+ },
+ enabled: !!purchaseInvoiceId,
+ });
+};
+
+export const usePurchase = (id) => {
+ return useQuery({
+ queryKey: ["purchase", id],
+ queryFn: async () => {
+ const resp = await PurchaseRepository.GetPurchase(id);
+ return resp.data;
+ },
+ enabled: !!id,
+ });
+};
+
+export const usePurchasePaymentHistory = (id) => {
+ return useQuery({
+ queryKey: ["purchase_payment_history", id],
+ queryFn: async () => {
+ const resp = await PurchaseRepository.GetPaymentHistory(id);
+ return resp.data;
+ },
+ enabled: !!id,
+ });
+};
+
+export const useCreatePurchaseInvoice = (onSuccessCallback) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (payload) =>
+ await PurchaseRepository.CreatePurchase(payload),
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["purchase_list"] });
+ showToast("Purchase Invoice Created successfully", "success");
+ if (onSuccessCallback) onSuccessCallback();
+ },
+ onError: (error) => {
+ showToast(
+ error?.response?.data?.message ||
+ error.message ||
+ "Failed to create invoice",
+ "error"
+ );
+ },
+ });
+};
+
+export const useUpdatePurchaseInvoice = (onSuccessCallback) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ purchaseId, payload }) =>
+ PurchaseRepository.UpdatePurchase(purchaseId, payload),
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["purchase_list"] });
+ queryClient.invalidateQueries({ queryKey: ["purchase"] });
+ showToast("Purchase Invoice Updated successfully", "success");
+ if (onSuccessCallback) onSuccessCallback();
+ },
+ onError: (error) => {
+ showToast(
+ error?.response?.data?.message ||
+ error.message ||
+ "Failed to create invoice",
+ "error"
+ );
+ },
+ });
+};
+export const useAddDeliverChallan = (onSuccessCallback) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (payload) =>
+ PurchaseRepository.addDelievryChallan(payload),
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["deliverliy_challane"] });
+ showToast("Challan added successfully", "success");
+ if (onSuccessCallback) onSuccessCallback();
+ },
+ onError: (error) => {
+ showToast(
+ error?.response?.data?.message ||
+ error.message ||
+ "Failed to create invoice",
+ "error"
+ );
+ },
+ });
+};
+export const useAddPurchasePayment =(onSuccessCallback)=>{
+const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (payload) =>
+ PurchaseRepository.AddPayment(payload),
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["purchase_payment_history"] });
+ showToast("Payment added successfully", "success");
+ if (onSuccessCallback) onSuccessCallback();
+ },
+ onError: (error) => {
+ showToast(
+ error?.response?.data?.message ||
+ error.message ||
+ "Failed to Add payment",
+ "error"
+ );
+ },
+ });
+}
+
+
+export const useDeletePurchaseInvoice = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ id, isActive }) =>
+ await PurchaseRepository.deletePurchase(id, isActive),
+
+ onSuccess: (_, variable) => {
+ queryClient.invalidateQueries({ queryKey: ["purchase_list"] });
+ showToast(
+ `Purchase Invoice ${variable.isActive ? "restored" : "deleted"} successfully`,
+ "success"
+ );
+ },
+
+ onError: (error) => {
+ showToast(
+ error?.response?.data?.message ||
+ error.message ||
+ "Failed to delete branch",
+ "error"
+ );
+ },
+ });
+};
diff --git a/src/hooks/useServiceProject.jsx b/src/hooks/useServiceProject.jsx
index 82bc5713..bac567c2 100644
--- a/src/hooks/useServiceProject.jsx
+++ b/src/hooks/useServiceProject.jsx
@@ -8,13 +8,14 @@ import { ServiceProjectRepository } from "../repositories/ServiceProjectReposito
import showToast from "../services/toastService";
//#region Service Project
-export const useServiceProjects = (pageSize, pageNumber) => {
+export const useServiceProjects = (pageSize, pageNumber, searchString) => {
return useQuery({
- queryKey: ["serviceProjects", pageSize, pageNumber],
+ queryKey: ["serviceProjects", pageSize, pageNumber, searchString],
queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProjects(
pageSize,
- pageNumber
+ pageNumber,
+ searchString
);
return response.data;
},
@@ -59,8 +60,8 @@ export const useCreateServiceProject = (onSuccessCallback) => {
onError: (error) => {
showToast(
error?.response?.data?.message ||
- error.message ||
- "Failed to delete task",
+ error.message ||
+ "Failed to delete task",
"error"
);
},
@@ -84,8 +85,8 @@ export const useUpdateServiceProject = (onSuccessCallback) => {
onError: (error) => {
showToast(
error?.response?.data?.message ||
- error.message ||
- "Failed to update project",
+ error.message ||
+ "Failed to update project",
"error"
);
},
@@ -110,8 +111,8 @@ export const useActiveInActiveServiceProject = (onSuccessCallback) => {
onError: (error) => {
showToast(
error?.response?.data?.message ||
- error.message ||
- "Failed to update project",
+ error.message ||
+ "Failed to update project",
"error"
);
},
@@ -138,8 +139,8 @@ export const useAllocationServiceProjectTeam = (onSuccessCallback) => {
onError: (error) => {
showToast(
error?.response?.data?.message ||
- error.message ||
- "Failed to update project",
+ error.message ||
+ "Failed to update project",
"error"
);
},
@@ -148,11 +149,6 @@ export const useAllocationServiceProjectTeam = (onSuccessCallback) => {
//#endregion
-
-
-
-
-
//#region Service Jobs
export const useServiceProjectJobs = (
@@ -163,7 +159,14 @@ export const useServiceProjectJobs = (
isArchive
) => {
return useQuery({
- queryKey: ["serviceProjectJobs", pageSize, pageNumber, isActive, project, isArchive],
+ queryKey: [
+ "serviceProjectJobs",
+ pageSize,
+ pageNumber,
+ isActive,
+ project,
+ isArchive,
+ ],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetJobList(
pageSize,
@@ -230,8 +233,8 @@ export const useAddCommentJob = (onSuccessCallback) => {
onError: (error) => {
showToast(
error?.response?.data?.message ||
- error.message ||
- "Failed to update project",
+ error.message ||
+ "Failed to update project",
"error"
);
},
@@ -254,8 +257,8 @@ export const useCreateServiceProjectJob = (onSuccessCallback) => {
onError: (error) => {
showToast(
error?.response?.data?.message ||
- error.message ||
- "Failed to update project",
+ error.message ||
+ "Failed to update project",
"error"
);
},
@@ -284,27 +287,19 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => {
}
},
-
onError: (error) => {
showToast(
error?.response?.data?.message ||
- error.message ||
- "Failed to update project",
+ error.message ||
+ "Failed to update project",
"error"
);
},
});
};
-
//#endregion
-
-
-
-
-
-
//#region Branch
export const useBranches = (
projectId,
@@ -343,8 +338,8 @@ export const useBranchTypes = () => {
const resp = await ServiceProjectRepository.GetBranchTypeList();
return resp.data;
},
- })
-}
+ });
+};
export const useBranchDetails = (id) => {
return useQuery({
@@ -353,9 +348,9 @@ export const useBranchDetails = (id) => {
const resp = await ServiceProjectRepository.GetBranchDetail(id);
return resp.data;
},
- enabled: !!id
- })
-}
+ enabled: !!id,
+ });
+};
export const useCreateBranch = (onSuccessCallBack) => {
const queryClient = useQueryClient();
@@ -401,7 +396,6 @@ export const useUpdateBranch = (onSuccessCallBack) => {
});
};
-
export const useDeleteBranch = () => {
const queryClient = useQueryClient();
@@ -411,14 +405,17 @@ export const useDeleteBranch = () => {
onSuccess: (_, variable) => {
queryClient.invalidateQueries({ queryKey: ["branches"] });
- showToast(`Branch ${variable.isActive ? "restored" : "deleted"} successfully`, "success");
+ showToast(
+ `Branch ${variable.isActive ? "restored" : "deleted"} successfully`,
+ "success"
+ );
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
- error.message ||
- "Failed to delete branch",
+ error.message ||
+ "Failed to delete branch",
"error"
);
},
diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx
index e7ac3638..d7b2b594 100644
--- a/src/pages/Expense/ExpensePage.jsx
+++ b/src/pages/Expense/ExpensePage.jsx
@@ -62,7 +62,7 @@ const ExpensePage = () => {
const [ViewDocument, setDocumentView] = useState({
IsOpen: false,
- Image: null,
+ Images: null,
});
const IsCreatedAble = useHasUserPermission(CREATE_EXEPENSE);
@@ -208,10 +208,10 @@ const ExpensePage = () => {
setDocumentView({ IsOpen: false, Image: null })}
+ key={ViewDocument.Images ?? "doc"}
+ closeModal={() => setDocumentView({ IsOpen: false, Images: null })}
>
-
+
)}
diff --git a/src/pages/ServiceProject/ServiceProjectDisplay.jsx b/src/pages/ServiceProject/ServiceProjectDisplay.jsx
index 1d59f1f2..6bb9299d 100644
--- a/src/pages/ServiceProject/ServiceProjectDisplay.jsx
+++ b/src/pages/ServiceProject/ServiceProjectDisplay.jsx
@@ -11,14 +11,18 @@ import GlobalModel from "../../components/common/GlobalModel";
import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject";
import { SpinnerLoader } from "../../components/common/Loader";
import ServiceProjectCard from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectCard";
+import ServiceProjectList from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectList";
+import { useDebounce } from "../../utils/appUtils";
-const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {
+const ServiceProjectDisplay = ({ listView, selectedStatuses, searchTerm }) => {
const [currentPage, setCurrentPage] = useState(1);
const { manageServiceProject, setManageServiceProject } = useProjectContext();
+ const debouncedSearch = useDebounce(searchTerm, 500);
const { data, isLoading, isError, error } = useServiceProjects(
ITEMS_PER_PAGE,
- currentPage
+ currentPage,
+ debouncedSearch
);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
@@ -47,15 +51,20 @@ const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {