Merge pull request 'Adding Upload functionlaiy in ManagePurchase.' (#524) from Multiple_File_Uploader into Purchase_Invoice_Management
Reviewed-on: #524 Merged
This commit is contained in:
commit
0482bfe191
@ -22,15 +22,15 @@ const Filelist = ({ files, removeFile, expenseToEdit, sm = 6, md = 4 }) => {
|
|||||||
style={{ minWidth: "30px" }}
|
style={{ minWidth: "30px" }}
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
<Tooltip text={file.fileName} >
|
<Tooltip text={file.fileName}>
|
||||||
<div className="d-flex flex-column text-truncate">
|
<div className="d-flex flex-column text-truncate">
|
||||||
<span className="fw-semibold small text-truncate">
|
<span className="fw-semibold small text-truncate">
|
||||||
{file.fileName}
|
{file.fileName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-body-secondary small">
|
<span className="text-body-secondary small">
|
||||||
{file.fileSize ? formatFileSize(file.fileSize) : ""}
|
{file.fileSize ? formatFileSize(file.fileSize) : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ const Filelist = ({ files, removeFile, expenseToEdit, sm = 6, md = 4 }) => {
|
|||||||
role="button"
|
role="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
removeFile(expenseToEdit ? file.documentId : idx);
|
removeFile(expenseToEdit ? file.documentId ?? file.tempId ?? idx : file.tempId ?? idx);
|
||||||
}}
|
}}
|
||||||
></i>
|
></i>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
16
src/components/InfoBlock/WarningBlock.jsx
Normal file
16
src/components/InfoBlock/WarningBlock.jsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const WarningBlock = ({content}) => {
|
||||||
|
return (
|
||||||
|
<div className="col-12 my-1">
|
||||||
|
<div className="d-flex align-items-center gap-2 p-3 rounded bg-warning bg-opacity-10 border border-warning-subtle text-start align-item-start">
|
||||||
|
<i className="bx bx-info-circle text-warning fs-5"></i>
|
||||||
|
<p className="mb-0 small">
|
||||||
|
{content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WarningBlock
|
||||||
@ -19,6 +19,8 @@ import SelectField from "../common/Forms/SelectField";
|
|||||||
import Filelist from "../Expenses/Filelist";
|
import Filelist from "../Expenses/Filelist";
|
||||||
import SingleFileUploader from "../common/SigleFileUploader";
|
import SingleFileUploader from "../common/SigleFileUploader";
|
||||||
import { localToUtc } from "../../utils/appUtils";
|
import { localToUtc } from "../../utils/appUtils";
|
||||||
|
import WarningBlock from "../InfoBlock/WarningBlock";
|
||||||
|
import { FILE_UPLOAD_INFO } from "../../utils/staticContent";
|
||||||
|
|
||||||
const DeliveryChallane = ({ purchaseId }) => {
|
const DeliveryChallane = ({ purchaseId }) => {
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
@ -178,15 +180,7 @@ const DeliveryChallane = ({ purchaseId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{!isUploaded && (
|
{!isUploaded && (
|
||||||
<div className="col-12 my-1">
|
<WarningBlock content={FILE_UPLOAD_INFO} />
|
||||||
<div className="d-flex align-items-center gap-2 p-3 rounded bg-warning bg-opacity-10 border border-warning-subtle text-start align-item-start">
|
|
||||||
<i className="bx bx-info-circle text-warning fs-5"></i>
|
|
||||||
<p className="mb-0 small">
|
|
||||||
If want upload document, Please select a document type before
|
|
||||||
uploading the document.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,44 +1,53 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useMemo, useCallback, useState } from "react";
|
||||||
import { AppFormProvider, useAppForm } from "../../hooks/appHooks/useAppForm";
|
import { AppFormProvider, useAppForm } from "../../hooks/appHooks/useAppForm";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultPurchaseValue,
|
defaultPurchaseValue,
|
||||||
PurchaseSchema,
|
PurchaseSchema,
|
||||||
getStepFields,
|
getStepFields,
|
||||||
} from "./PurchaseSchema";
|
} from "./PurchaseSchema";
|
||||||
|
|
||||||
import PurchasePartyDetails from "./PurchasePartyDetails";
|
import PurchasePartyDetails from "./PurchasePartyDetails";
|
||||||
import PurchaseTransportDetails from "./PurchaseTransportDetails";
|
import PurchaseTransportDetails from "./PurchaseTransportDetails";
|
||||||
import PurchasePaymentDetails from "./PurchasePaymentDetails";
|
import PurchasePaymentDetails from "./PurchasePaymentDetails";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useCreatePurchaseInvoice,
|
useCreatePurchaseInvoice,
|
||||||
usePurchase,
|
usePurchase,
|
||||||
useUpdatePurchaseInvoice,
|
useUpdatePurchaseInvoice,
|
||||||
} from "../../hooks/usePurchase";
|
} from "../../hooks/usePurchase";
|
||||||
|
import { error } from "pdf-lib";
|
||||||
|
|
||||||
const ManagePurchase = ({ onClose, purchaseId }) => {
|
const ManagePurchase = ({ onClose, purchaseId }) => {
|
||||||
const { data } = usePurchase(purchaseId);
|
const { data } = usePurchase(purchaseId);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const [completedTabs, setCompletedTabs] = useState([]);
|
const [completedTabs, setCompletedTabs] = useState([]);
|
||||||
|
|
||||||
const stepsConfig = [
|
const stepsConfig = useMemo(
|
||||||
{
|
() => [
|
||||||
name: "Party Details",
|
{
|
||||||
icon: "bx bx-user bx-md",
|
name: "Party Details",
|
||||||
subtitle: "Supplier & project information",
|
icon: "bx bx-user bx-md",
|
||||||
component: <PurchasePartyDetails purchase={data} />,
|
subtitle: "Supplier & project information",
|
||||||
},
|
component: <PurchasePartyDetails purchase={data} />,
|
||||||
{
|
},
|
||||||
name: "Invoice & Transport",
|
{
|
||||||
icon: "bx bx-receipt bx-md",
|
name: "Invoice & Transport",
|
||||||
subtitle: "Invoice, eWay bill & transport info",
|
icon: "bx bx-receipt bx-md",
|
||||||
component: <PurchaseTransportDetails />,
|
subtitle: "Invoice, eWay bill & transport info",
|
||||||
},
|
component: <PurchaseTransportDetails />,
|
||||||
{
|
},
|
||||||
name: "Payment Details",
|
{
|
||||||
icon: "bx bx-credit-card bx-md",
|
name: "Payment Details",
|
||||||
subtitle: "Amount, tax & due date",
|
icon: "bx bx-credit-card bx-md",
|
||||||
component: <PurchasePaymentDetails />,
|
subtitle: "Amount, tax & due date",
|
||||||
},
|
component: <PurchasePaymentDetails purchaseId={purchaseId} />,
|
||||||
];
|
},
|
||||||
|
],
|
||||||
|
[data, purchaseId]
|
||||||
|
);
|
||||||
|
|
||||||
const purchaseOrder = useAppForm({
|
const purchaseOrder = useAppForm({
|
||||||
resolver: zodResolver(PurchaseSchema),
|
resolver: zodResolver(PurchaseSchema),
|
||||||
@ -47,42 +56,63 @@ const ManagePurchase = ({ onClose, purchaseId }) => {
|
|||||||
shouldUnregister: false,
|
shouldUnregister: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { reset, formState } = purchaseOrder;
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = purchaseOrder;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (purchaseId && data) {
|
if (!purchaseId || !data) return;
|
||||||
reset({
|
|
||||||
...data,
|
reset({
|
||||||
title: data.title,
|
...data,
|
||||||
projectId: data.project.id,
|
projectId: data?.project?.id,
|
||||||
organizationId: data.organization.id,
|
organizationId: data?.organization?.id,
|
||||||
supplierId: data.supplier.id,
|
supplierId: data?.supplier?.id,
|
||||||
});
|
invoiceAttachmentTypeId: null,
|
||||||
setCompletedTabs([0, 1, 2]);
|
attachments:
|
||||||
}
|
data?.attachments?.map((doc) => ({
|
||||||
|
fileName: doc.fileName,
|
||||||
|
base64Data: null,
|
||||||
|
contentType: doc.contentType,
|
||||||
|
documentId: doc.documentId,
|
||||||
|
invoiceAttachmentTypeId: doc.invoiceAttachmentType?.id ?? null,
|
||||||
|
fileSize: 0,
|
||||||
|
description: "",
|
||||||
|
preSignedUrl: doc.preSignedUrl,
|
||||||
|
isActive: doc.isActive ?? true,
|
||||||
|
})) || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
setCompletedTabs([0, 1, 2]);
|
||||||
}, [purchaseId, data, reset]);
|
}, [purchaseId, data, reset]);
|
||||||
|
|
||||||
const handleNext = async (e) => {
|
const handleNext = useCallback(async () => {
|
||||||
e.preventDefault();
|
const fields = getStepFields(activeTab);
|
||||||
e.stopPropagation();
|
const valid = await purchaseOrder.trigger(fields);
|
||||||
|
|
||||||
const currentStepFields = getStepFields(activeTab);
|
if (!valid) return;
|
||||||
const valid = await purchaseOrder.trigger(currentStepFields);
|
|
||||||
|
|
||||||
if (valid) {
|
setCompletedTabs((prev) => [...new Set([...prev, activeTab])]);
|
||||||
setCompletedTabs((prev) => [...new Set([...prev, activeTab])]);
|
setActiveTab((prev) => Math.min(prev + 1, stepsConfig.length - 1));
|
||||||
setActiveTab((prev) => Math.min(prev + 1, stepsConfig.length - 1));
|
}, [activeTab, purchaseOrder, stepsConfig.length]);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrev = (e) => {
|
const handlePrev = useCallback(() => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setActiveTab((prev) => Math.max(prev - 1, 0));
|
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(() => {
|
const { mutate: CreateInvoice, isPending } = useCreatePurchaseInvoice(() => {
|
||||||
reset();
|
reset();
|
||||||
@ -95,42 +125,22 @@ const ManagePurchase = ({ onClose, purchaseId }) => {
|
|||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = useCallback(
|
||||||
if (activeTab !== 2) {
|
(formData) => {
|
||||||
console.warn("Submit blocked - not on last step");
|
if (activeTab !== 2) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirtyFields = purchaseOrder.formState.dirtyFields;
|
if (purchaseId) {
|
||||||
|
const payload = generatePatchOps(formData);
|
||||||
if (purchaseId) {
|
updatePurchase({ purchaseId, payload });
|
||||||
const changedData = Object.keys(dirtyFields).reduce((acc, key) => {
|
} else {
|
||||||
if (dirtyFields[key]) {
|
CreateInvoice(formData);
|
||||||
acc.push({
|
}
|
||||||
operationType: 0,
|
},
|
||||||
path: `/${key}`,
|
[activeTab, purchaseId, generatePatchOps, updatePurchase, CreateInvoice]
|
||||||
op: "replace",
|
);
|
||||||
from: null,
|
|
||||||
value: formData[key],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
updatePurchase({
|
|
||||||
purchaseId,
|
|
||||||
payload: changedData,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
CreateInvoice(formData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bs-stepper horizontically mt-2 b-secondry shadow-none border-0">
|
||||||
id="wizard-property-listing"
|
{/* --- Steps Header --- */}
|
||||||
className="bs-stepper horizontically mt-2 b-secondry shadow-none border-0"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="bs-stepper-header text-start px-0 py-1">
|
<div className="bs-stepper-header text-start px-0 py-1">
|
||||||
{stepsConfig.map((step, index) => {
|
{stepsConfig.map((step, index) => {
|
||||||
const isActive = activeTab === index;
|
const isActive = activeTab === index;
|
||||||
@ -143,7 +153,11 @@ const ManagePurchase = ({ onClose, purchaseId }) => {
|
|||||||
isCompleted ? "crossed" : ""
|
isCompleted ? "crossed" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button type="button" className="step-trigger">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="step-trigger"
|
||||||
|
onClick={() => purchaseId && setActiveTab(index)}
|
||||||
|
>
|
||||||
<span className="bs-stepper-circle">
|
<span className="bs-stepper-circle">
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
<i className="bx bx-check"></i>
|
<i className="bx bx-check"></i>
|
||||||
@ -167,28 +181,18 @@ const ManagePurchase = ({ onClose, purchaseId }) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* --- Form Content --- */}
|
||||||
<div className="bs-stepper-content py-2 px-3">
|
<div className="bs-stepper-content py-2 px-3">
|
||||||
<AppFormProvider {...purchaseOrder}>
|
<AppFormProvider {...purchaseOrder}>
|
||||||
<form
|
<form
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) =>
|
||||||
if (e.key === "Enter" && activeTab !== 2) {
|
e.key === "Enter" && activeTab !== 2 && e.preventDefault()
|
||||||
e.preventDefault();
|
}
|
||||||
}
|
onSubmit={purchaseOrder.handleSubmit(onSubmit)}
|
||||||
}}
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (activeTab !== 2) {
|
|
||||||
console.warn("BLOCKED SUBMIT on step:", activeTab);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
purchaseOrder.handleSubmit(onSubmit)(e);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{stepsConfig[activeTab].component}
|
{stepsConfig[activeTab].component}
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
<div className="d-flex justify-content-between mt-4">
|
<div className="d-flex justify-content-between mt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -207,7 +211,7 @@ const ManagePurchase = ({ onClose, purchaseId }) => {
|
|||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
disabled={isPending || isUpdating}
|
disabled={isPending || isUpdating}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -74,7 +74,7 @@ const PurchaseList = ({ searchString }) => {
|
|||||||
{col.render ? col.render(item) : item[col.key] || "NA"}
|
{col.render ? col.render(item) : item[col.key] || "NA"}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
<td>
|
<td >
|
||||||
<div className="dropdown z-2">
|
<div className="dropdown z-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -1,11 +1,21 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
import { useAppFormContext } from "../../hooks/appHooks/useAppForm";
|
import {
|
||||||
|
AppFormController,
|
||||||
|
useAppFormContext,
|
||||||
|
} from "../../hooks/appHooks/useAppForm";
|
||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../common/DatePicker";
|
||||||
import { useInvoiceAttachmentTypes } from "../../hooks/masterHook/useMaster";
|
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 = () => {
|
const PurchasePaymentDetails = ({ purchaseId = null }) => {
|
||||||
const { data, isLoading, error, isError } = useInvoiceAttachmentTypes();
|
const { data, isLoading, error, isError } = useInvoiceAttachmentTypes();
|
||||||
|
const { data: InvoiceDocTypes, isLoading: invoiceDocLoading } =
|
||||||
|
useInvoiceAttachmentTypes();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
watch,
|
watch,
|
||||||
@ -25,9 +35,78 @@ const PurchasePaymentDetails = () => {
|
|||||||
setValue("totalAmount", (base + tax).toFixed(2));
|
setValue("totalAmount", (base + tax).toFixed(2));
|
||||||
}
|
}
|
||||||
}, [baseAmount, taxAmount, setValue]);
|
}, [baseAmount, taxAmount, 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 (
|
return (
|
||||||
<div className="row g-3 text-start">
|
<div className="row g-1 text-start">
|
||||||
<div className="col-12 col-md-4">
|
<div className="col-12 col-md-4">
|
||||||
<Label htmlFor="baseAmount" required>
|
<Label htmlFor="baseAmount" required>
|
||||||
Base Amount
|
Base Amount
|
||||||
@ -56,7 +135,7 @@ const PurchasePaymentDetails = () => {
|
|||||||
id="taxAmount"
|
id="taxAmount"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control form-control-xs"
|
className="form-control form-control-xs"
|
||||||
{...register("taxAmount",{ valueAsNumber: true })}
|
{...register("taxAmount", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{errors?.taxAmount && (
|
{errors?.taxAmount && (
|
||||||
@ -75,7 +154,7 @@ const PurchasePaymentDetails = () => {
|
|||||||
id="totalAmount"
|
id="totalAmount"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control form-control-xs"
|
className="form-control form-control-xs"
|
||||||
{...register("totalAmount",{ valueAsNumber: true })}
|
{...register("totalAmount", { valueAsNumber: true })}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -93,7 +172,7 @@ const PurchasePaymentDetails = () => {
|
|||||||
id="transportCharges"
|
id="transportCharges"
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control form-control-xs"
|
className="form-control form-control-xs"
|
||||||
{...register("transportCharges",{ valueAsNumber: true })}
|
{...register("transportCharges", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{errors?.transportCharges && (
|
{errors?.transportCharges && (
|
||||||
@ -138,6 +217,90 @@ const PurchasePaymentDetails = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<AppFormController
|
||||||
|
name="invoiceAttachmentTypeId"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<SelectField
|
||||||
|
label="Select Document Type"
|
||||||
|
options={InvoiceDocTypes ?? []}
|
||||||
|
placeholder="Choose Type"
|
||||||
|
labelKeyKey="name"
|
||||||
|
valueKeyKey="id"
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
isLoading={invoiceDocLoading}
|
||||||
|
className="m-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errors?.invoiceAttachmentTypeId && (
|
||||||
|
<small className="danger-text">
|
||||||
|
{errors.invoiceAttachmentTypeId.message}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-start">
|
||||||
|
<div className="col-md-12">
|
||||||
|
<Label className="form-label" required>
|
||||||
|
Upload Bill{" "}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={() => document.getElementById("attachments").click()}
|
||||||
|
>
|
||||||
|
<i className="bx bx-cloud-upload d-block bx-lg"> </i>
|
||||||
|
<span className="text-muted d-block">
|
||||||
|
Click to select or click here to browse
|
||||||
|
</span>
|
||||||
|
<small className="text-muted">(PDF, JPG, PNG, max 5MB)</small>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="attachments"
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
multiple
|
||||||
|
disabled={!invoiceAttachmentType}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
{...register("attachments")}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFileChange(e);
|
||||||
|
e.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.attachments && (
|
||||||
|
<small className="danger-text">{errors.attachments.message}</small>
|
||||||
|
)}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<Filelist
|
||||||
|
files={files}
|
||||||
|
removeFile={removeFile}
|
||||||
|
expenseToEdit={purchaseId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{Array.isArray(errors.attachments) &&
|
||||||
|
errors.attachments.map((fileError, index) => (
|
||||||
|
<div key={index} className="danger-text small mt-1">
|
||||||
|
{
|
||||||
|
(fileError?.fileSize?.message ||
|
||||||
|
fileError?.contentType?.message ||
|
||||||
|
fileError?.base64Data?.message,
|
||||||
|
fileError?.documentId?.message)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!invoiceAttachmentType && (
|
||||||
|
<WarningBlock content={FILE_UPLOAD_INFO} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,39 +1,30 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { normalizeAllowedContentTypes } from "../../utils/appUtils";
|
import { normalizeAllowedContentTypes } from "../../utils/appUtils";
|
||||||
|
|
||||||
export const AttachmentSchema = (allowedContentType, maxSizeAllowedInMB) => {
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
const allowedTypes = normalizeAllowedContentTypes(allowedContentType);
|
const ALLOWED_TYPES = [
|
||||||
|
"application/pdf",
|
||||||
|
"image/png",
|
||||||
|
"image/jpg",
|
||||||
|
"image/jpeg",
|
||||||
|
];
|
||||||
|
|
||||||
return z.object({
|
export const AttachmentSchema = z.object({
|
||||||
fileName: z.string().min(1, { message: "File name is required" }),
|
invoiceAttachmentTypeId: z.string().nullable(),
|
||||||
base64Data: z.string().min(1, { message: "File data is required" }),
|
fileName: z.string().min(1, { message: "Filename is required" }),
|
||||||
invoiceAttachmentTypeId: z
|
base64Data: z.string().nullable(),
|
||||||
.string()
|
contentType: z
|
||||||
.min(1, { message: "File data is required" }),
|
.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),
|
||||||
|
});
|
||||||
|
|
||||||
contentType: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "MIME type is required" })
|
|
||||||
.refine(
|
|
||||||
(val) => (allowedTypes.length ? allowedTypes.includes(val) : true),
|
|
||||||
{
|
|
||||||
message: `File type must be one of: ${allowedTypes.join(", ")}`,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
fileSize: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.nonnegative("fileSize must be ≥ 0")
|
|
||||||
.max(
|
|
||||||
(maxSizeAllowedInMB ?? 25) * 1024 * 1024,
|
|
||||||
`fileSize must be ≤ ${maxSizeAllowedInMB ?? 25}MB`
|
|
||||||
),
|
|
||||||
|
|
||||||
description: z.string().optional().default(""),
|
|
||||||
isActive: z.boolean(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PurchaseSchema = z.object({
|
export const PurchaseSchema = z.object({
|
||||||
title: z.string().min(1, { message: "Title is required" }),
|
title: z.string().min(1, { message: "Title is required" }),
|
||||||
@ -65,15 +56,13 @@ export const PurchaseSchema = z.object({
|
|||||||
paymentDueDate: z.coerce.date().nullable(),
|
paymentDueDate: z.coerce.date().nullable(),
|
||||||
transportCharges: z.number().nullable(),
|
transportCharges: z.number().nullable(),
|
||||||
description: z.string().min(1, { message: "Description is required" }),
|
description: z.string().min(1, { message: "Description is required" }),
|
||||||
|
invoiceAttachmentTypeId:z.string().nullable(),
|
||||||
|
attachments: z
|
||||||
|
.array(AttachmentSchema)
|
||||||
|
|
||||||
|
|
||||||
// attachments: z.array(AttachmentSchema([], 25)).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// deliveryChallanNo: z
|
|
||||||
// .string()
|
|
||||||
// .min(1, { message: "Delivery Challan No is required" }),
|
|
||||||
// deliveryDate: z.string().min(1, { message: "Delevery Date is required" }),
|
|
||||||
// shippingAddress: z.string().min(1, { message: "Delevery Date is required" }),
|
|
||||||
|
|
||||||
export const defaultPurchaseValue = {
|
export const defaultPurchaseValue = {
|
||||||
title: "",
|
title: "",
|
||||||
@ -104,8 +93,8 @@ export const defaultPurchaseValue = {
|
|||||||
paymentDueDate: null,
|
paymentDueDate: null,
|
||||||
transportCharges: null,
|
transportCharges: null,
|
||||||
description: "",
|
description: "",
|
||||||
|
invoiceAttachmentTypeId:null,
|
||||||
// attachments: [],
|
attachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStepFields = (stepIndex) => {
|
export const getStepFields = (stepIndex) => {
|
||||||
@ -138,7 +127,9 @@ export const getStepFields = (stepIndex) => {
|
|||||||
"totalAmount",
|
"totalAmount",
|
||||||
"transportCharges",
|
"transportCharges",
|
||||||
"paymentDueDate",
|
"paymentDueDate",
|
||||||
|
"invoiceAttachmentTypeId",
|
||||||
"description",
|
"description",
|
||||||
|
"attachments"
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -180,7 +171,7 @@ export const DeliveryChallanSchema = z.object({
|
|||||||
invoiceAttachmentTypeId: z.string().nullable(),
|
invoiceAttachmentTypeId: z.string().nullable(),
|
||||||
deliveryChallanDate: z.string().min(1, { message: "Deliver date required" }),
|
deliveryChallanDate: z.string().min(1, { message: "Deliver date required" }),
|
||||||
description: z.string().min(1, { message: "Description required" }),
|
description: z.string().min(1, { message: "Description required" }),
|
||||||
attachment: z.any().refine(
|
attachment: z.any().refine(
|
||||||
(val) => val && typeof val === "object" && !!val.base64Data,
|
(val) => val && typeof val === "object" && !!val.base64Data,
|
||||||
{
|
{
|
||||||
message: "Please upload document",
|
message: "Please upload document",
|
||||||
|
|||||||
@ -26,13 +26,13 @@ export const PurchaseColumn = [
|
|||||||
{
|
{
|
||||||
key: "project",
|
key: "project",
|
||||||
label: "Project",
|
label: "Project",
|
||||||
className: "text-start d-none d-sm-table-cell",
|
className: "text-start ",
|
||||||
render: (item) => <span>{item?.project?.name || "NA"}</span>,
|
render: (item) => <span>{item?.project?.name || "NA"}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "supplier",
|
key: "supplier",
|
||||||
label: "Supplier",
|
label: "Supplier",
|
||||||
className: "text-start d-none d-sm-table-cell",
|
className: "text-start ",
|
||||||
render: (item) => <span>{item?.supplier?.name || "NA"}</span>,
|
render: (item) => <span>{item?.supplier?.name || "NA"}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -49,7 +49,7 @@ const PurchasePage = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="card px-sm-4 my-3">
|
<div className="card px-sm-4 my-3">
|
||||||
<div className="row p-2">
|
<div className="row p-2">
|
||||||
<div className="col-12 col-md-6 text-start">
|
<div className="col-12 col-sm-6 text-start">
|
||||||
{" "}
|
{" "}
|
||||||
<label className="mb-0">
|
<label className="mb-0">
|
||||||
<input
|
<input
|
||||||
@ -62,7 +62,7 @@ const PurchasePage = () => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<di className="col-6 text-end">
|
<di className="col-sm-6 text-end">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
4
src/utils/staticContent.jsx
Normal file
4
src/utils/staticContent.jsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const FILE_UPLOAD_INFO = `
|
||||||
|
If you want to upload a document, please select a document type
|
||||||
|
before uploading the document.
|
||||||
|
`;
|
||||||
Loading…
x
Reference in New Issue
Block a user