-
+
{
))}
);
-};
\ No newline at end of file
+};
+export const FileView = ({ file, viewFile }) => {
+ if (!file) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {/* File icon and info */}
+
+
+
+
{
+ e.preventDefault();
+ viewFile({
+ IsOpen: true,
+ Image: file.preSignedUrl,
+ });
+ }}
+ >
+
+ {file.fileName}
+
+
+
+ {" "}
+ {file.fileSize ? formatFileSize(file.fileSize) : ""}
+
+
+
+
+
+
+ );
+};
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..d8da640c
--- /dev/null
+++ b/src/components/purchase/DeliveryChallane.jsx
@@ -0,0 +1,205 @@
+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";
+
+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 && (
+
+
+
+
+ If want upload document, Please select a document type before
+ uploading the document.
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default DeliveryChallane;
diff --git a/src/components/purchase/PurchaseList.jsx b/src/components/purchase/PurchaseList.jsx
index 7c51f7a3..cc98c9aa 100644
--- a/src/components/purchase/PurchaseList.jsx
+++ b/src/components/purchase/PurchaseList.jsx
@@ -8,7 +8,8 @@ import { useDebounce } from "../../utils/appUtils";
import { usePurchaseContext } from "../../pages/purchase/PurchasePage";
const PurchaseList = ({ searchString }) => {
- const { setViewPurchase, setManagePurchase } = usePurchaseContext();
+ const { setViewPurchase, setManagePurchase, setChallan } =
+ usePurchaseContext();
const [currentPage, setCurrentPage] = useState(1);
const debounceSearch = useDebounce(searchString, 300);
const { data, isLoading } = usePurchasesList(
@@ -120,7 +121,15 @@ const PurchaseList = ({ searchString }) => {
-
+
+ setChallan({
+ isOpen: true,
+ purchaseId: item.id,
+ })
+ }
+ >
Delete
diff --git a/src/components/purchase/PurchaseSchema.jsx b/src/components/purchase/PurchaseSchema.jsx
index 33d41bcb..83fd88f8 100644
--- a/src/components/purchase/PurchaseSchema.jsx
+++ b/src/components/purchase/PurchaseSchema.jsx
@@ -144,3 +144,49 @@ export const getStepFields = (stepIndex) => {
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.object(SingleAttachmentSchema.shape).nullable(),
+});
+
+export const DeliveryChallanDefaultValue = {
+ deliveryChallanNumber: "",
+ deliveryChallanDate: "",
+ description: "",
+ attachment: null,
+ invoiceAttachmentTypeId: null,
+};
diff --git a/src/hooks/usePurchase.jsx b/src/hooks/usePurchase.jsx
index aa535b58..adb767df 100644
--- a/src/hooks/usePurchase.jsx
+++ b/src/hooks/usePurchase.jsx
@@ -31,6 +31,19 @@ export const usePurchasesList = (
});
};
+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],
@@ -67,7 +80,8 @@ export const useUpdatePurchaseInvoice = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: async ({purchaseId,payload}) => PurchaseRepository.UpdatePurchase(purchaseId, payload),
+ mutationFn: async ({ purchaseId, payload }) =>
+ PurchaseRepository.UpdatePurchase(purchaseId, payload),
onSuccess: (data, variables) => {
showToast("Purchase Invoice Updated successfully", "success");
if (onSuccessCallback) onSuccessCallback();
@@ -82,3 +96,24 @@ export const useUpdatePurchaseInvoice = (onSuccessCallback) => {
},
});
};
+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"
+ );
+ },
+ });
+};
diff --git a/src/pages/purchase/PurchasePage.jsx b/src/pages/purchase/PurchasePage.jsx
index 8b3c5036..8edcb845 100644
--- a/src/pages/purchase/PurchasePage.jsx
+++ b/src/pages/purchase/PurchasePage.jsx
@@ -5,6 +5,7 @@ import GlobalModel from "../../components/common/GlobalModel";
import ManagePurchase from "../../components/purchase/ManagePurchase";
import PurchaseList from "../../components/purchase/PurchaseList";
import ViewPurchase from "../../components/purchase/ViewPurchase";
+import DeliveryChallane from "../../components/purchase/DeliveryChallane";
export const PurchaseContext = createContext();
export const usePurchaseContext = () => {
@@ -18,6 +19,10 @@ export const usePurchaseContext = () => {
};
const PurchasePage = () => {
const [searchText, setSearchText] = useState("");
+ const [addChallan, setChallan] = useState({
+ isOpen: false,
+ purchaseId: null,
+ });
const [managePurchase, setManagePurchase] = useState({
isOpen: false,
purchaseId: null,
@@ -30,6 +35,7 @@ const PurchasePage = () => {
const contextValue = {
setViewPurchase,
setManagePurchase,
+ setChallan,
};
return (
@@ -107,6 +113,18 @@ const PurchasePage = () => {
)}
+
+ {addChallan.isOpen && (
+ setChallan({ isOpen: false, purchaseId: null })}
+ >
+ setChallan({ isOpen: false, purchaseId: null })}
+ />
+
+ )}