306 lines
8.9 KiB
JavaScript
306 lines
8.9 KiB
JavaScript
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,
|
|
});
|
|
};
|
|
const hasNumber = !!watch("proformaInvoiceNumber");
|
|
return (
|
|
<div className="row g-1 text-start">
|
|
<div className="col-12 col-md-4">
|
|
<Label htmlFor="baseAmount" required={!hasNumber}>
|
|
Base Amount
|
|
</Label>
|
|
|
|
<input
|
|
id="baseAmount"
|
|
type="number"
|
|
className="form-control form-control-xs"
|
|
{...register("baseAmount", { valueAsNumber: true })}
|
|
/>
|
|
|
|
{errors?.baseAmount && (
|
|
<div className="small danger-text mt-1">
|
|
{errors.baseAmount.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="col-12 col-md-4">
|
|
<Label htmlFor="taxAmount" required={!hasNumber}>
|
|
Tax Amount
|
|
</Label>
|
|
|
|
<input
|
|
id="taxAmount"
|
|
type="number"
|
|
className="form-control form-control-xs"
|
|
{...register("taxAmount", { valueAsNumber: true })}
|
|
/>
|
|
|
|
{errors?.taxAmount && (
|
|
<div className="small danger-text mt-1">
|
|
{errors.taxAmount.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-4">
|
|
<Label htmlFor="transportCharges">Transport Charges</Label>
|
|
|
|
<input
|
|
id="transportCharges"
|
|
type="number"
|
|
className="form-control form-control-xs"
|
|
{...register("transportCharges", { valueAsNumber: true })}
|
|
/>
|
|
|
|
{errors?.transportCharges && (
|
|
<div className="small danger-text mt-1">
|
|
{errors.transportCharges.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="col-12 col-md-6">
|
|
<Label htmlFor="totalAmount" required={!hasNumber}>
|
|
Total Amount
|
|
</Label>
|
|
|
|
<input
|
|
id="totalAmount"
|
|
type="number"
|
|
className="form-control form-control-xs"
|
|
{...register("totalAmount", { valueAsNumber: true })}
|
|
/>
|
|
|
|
{errors?.totalAmount && (
|
|
<div className="small danger-text mt-1">
|
|
{errors.totalAmount.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="col-12 col-md-6">
|
|
<Label htmlFor="paymentDueDate">Payment Due Date</Label>
|
|
|
|
<DatePicker
|
|
name={"paymentDueDate"}
|
|
control={control}
|
|
className="w-full"
|
|
size="xs"
|
|
/>
|
|
|
|
{errors?.paymentDueDate && (
|
|
<div className="small danger-text mt-1">
|
|
{errors.paymentDueDate.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="col-12 mb-2">
|
|
<Label htmlFor="description">Description</Label>
|
|
|
|
<textarea
|
|
id="description"
|
|
rows="3"
|
|
className="form-control form-control-xs"
|
|
{...register("description")}
|
|
/>
|
|
|
|
{errors?.description && (
|
|
<div className="small danger-text mt-1">
|
|
{errors.description.message}
|
|
</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>
|
|
);
|
|
};
|
|
|
|
export default PurchasePaymentDetails;
|