435 lines
14 KiB
JavaScript
435 lines
14 KiB
JavaScript
import React, { useEffect } from "react";
|
|
import { useModal } from "../../hooks/useAuth";
|
|
import Modal from "../common/Modal";
|
|
import { FormProvider, useForm } from "react-hook-form";
|
|
import Label from "../common/Label";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { defaultCollection, newCollection } from "./collectionSchema";
|
|
import SelectMultiple from "../common/SelectMultiple";
|
|
import { useProjectName } from "../../hooks/useProjects";
|
|
import DatePicker from "../common/DatePicker";
|
|
import {
|
|
useCollection,
|
|
useCreateCollection,
|
|
useUpdateCollection,
|
|
} from "../../hooks/useCollections";
|
|
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
|
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
|
import { formatDate } from "../../utils/dateUtils";
|
|
|
|
const ManageCollection = ({ collectionId, onClose }) => {
|
|
const { data, isError, isLoading, error } = useCollection(collectionId);
|
|
const { projectNames, projectLoading } = useProjectName();
|
|
const methods = useForm({
|
|
resolver: zodResolver(newCollection),
|
|
defaultValues: defaultCollection,
|
|
});
|
|
const {
|
|
control,
|
|
watch,
|
|
register,
|
|
setValue,
|
|
handleSubmit,
|
|
reset,
|
|
formState: { errors },
|
|
} = methods;
|
|
|
|
const { mutate: createNewCollection, isPending } = useCreateCollection(() => {
|
|
handleClose();
|
|
});
|
|
const { mutate: UpdateCollection } = useUpdateCollection(() => {
|
|
handleClose();
|
|
});
|
|
|
|
const files = watch("attachments");
|
|
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,
|
|
};
|
|
})
|
|
);
|
|
|
|
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 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 removeFile = (index) => {
|
|
if (collectionId) {
|
|
const newFiles = files.map((file, i) => {
|
|
if (file.documentId !== index) return file;
|
|
return {
|
|
...file,
|
|
isActive: false,
|
|
};
|
|
});
|
|
setValue("attachments", newFiles, { shouldValidate: true });
|
|
} else {
|
|
const newFiles = files.filter((_, i) => i !== index);
|
|
setValue("attachments", newFiles, { shouldValidate: true });
|
|
}
|
|
};
|
|
|
|
const onSubmit = (formData) => {
|
|
const payload = {
|
|
...formData,
|
|
clientSubmitedDate: localToUtc(formData.clientSubmitedDate),
|
|
invoiceDate: localToUtc(formData.invoiceDate),
|
|
exceptedPaymentDate: localToUtc(formData.exceptedPaymentDate),
|
|
};
|
|
|
|
if (collectionId) {
|
|
UpdateCollection({
|
|
collectionId,
|
|
payload: { ...payload, id: collectionId },
|
|
});
|
|
} else {
|
|
createNewCollection(payload);
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
reset(defaultCollection);
|
|
onClose();
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (data && collectionId) {
|
|
reset({
|
|
projectId: data?.project?.id,
|
|
invoiceNumber: data?.invoiceNumber,
|
|
eInvoiceNumber: data?.eInvoiceNumber,
|
|
title: data?.title,
|
|
clientSubmitedDate: formatDate(data?.clientSubmitedDate),
|
|
invoiceDate: formatDate(data?.invoiceDate),
|
|
exceptedPaymentDate: formatDate(data?.exceptedPaymentDate),
|
|
taxAmount: data?.taxAmount,
|
|
basicAmount: data?.basicAmount,
|
|
description: data?.description,
|
|
attachments: data?.attachments,
|
|
attachments: data.attachments
|
|
? data.attachments.map((doc) => ({
|
|
fileName: doc.fileName,
|
|
base64Data: null,
|
|
contentType: doc.contentType,
|
|
documentId: doc.documentId,
|
|
fileSize: 0,
|
|
description: "",
|
|
preSignedUrl: doc.preSignedUrl,
|
|
isActive: doc.isActive ?? true,
|
|
}))
|
|
: [],
|
|
});
|
|
}
|
|
}, [data]);
|
|
if (isLoading) return <div>Loading....</div>;
|
|
if (isError) return <div>{error.message}</div>;
|
|
return (
|
|
<div className="container pb-3">
|
|
<div className="text-black fs-5 mb-2">
|
|
{collectionId ? "Update" : "New"} Collection
|
|
</div>
|
|
<FormProvider {...methods}>
|
|
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
|
|
<div className="row px-md-1 px-0">
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label required>Title</Label>
|
|
<input
|
|
type="text"
|
|
className="form-control form-control-sm"
|
|
{...register("title")}
|
|
/>
|
|
{errors.title && (
|
|
<small className="danger-text">{errors.title.message}</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label required>Invoice Number</Label>
|
|
<input
|
|
type="text"
|
|
className="form-control form-control-sm"
|
|
{...register("invoiceNumber")}
|
|
/>
|
|
{errors.invoiceId && (
|
|
<small className="danger-text">
|
|
{errors.invoiceId.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label required>E-Invoice Number</Label>
|
|
<input
|
|
type="text"
|
|
className="form-control form-control-sm"
|
|
{...register("eInvoiceNumber")}
|
|
/>
|
|
{errors.invoiceId && (
|
|
<small className="danger-text">
|
|
{errors.invoiceId.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label required>Invoice Date</Label>
|
|
<DatePicker
|
|
name="invoiceDate"
|
|
control={control}
|
|
maxDate={new Date()}
|
|
/>
|
|
{errors.invoiceDate && (
|
|
<small className="danger-text">
|
|
{errors.invoiceDate.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label required>Expected Date</Label>
|
|
<DatePicker
|
|
name="exceptedPaymentDate"
|
|
control={control}
|
|
minDate={watch("invoiceDate")}
|
|
/>
|
|
{errors.exceptedPaymentDate && (
|
|
<small className="danger-text">
|
|
{errors.exceptedPaymentDate.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label required>Submission Date (Client)</Label>
|
|
<DatePicker
|
|
name="clientSubmitedDate"
|
|
control={control}
|
|
maxDate={new Date()}
|
|
/>
|
|
{errors.exceptedPaymentDate && (
|
|
<small className="danger-text">
|
|
{errors.exceptedPaymentDate.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label className="form-label" required>
|
|
Select Project
|
|
</Label>
|
|
<select
|
|
className="form-select form-select-sm"
|
|
{...register("projectId")}
|
|
>
|
|
<option value="">Select Project</option>
|
|
{projectLoading ? (
|
|
<option>Loading...</option>
|
|
) : (
|
|
projectNames?.map((project) => (
|
|
<option key={project.id} value={project.id}>
|
|
{project.name}
|
|
</option>
|
|
))
|
|
)}
|
|
</select>
|
|
{errors.projectId && (
|
|
<small className="danger-text">
|
|
{errors.projectId.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label htmlFor="basicAmount" className="form-label" required>
|
|
Amount
|
|
</Label>
|
|
<input
|
|
type="number"
|
|
id="basicAmount"
|
|
className="form-control form-control-sm"
|
|
min="1"
|
|
step="0.01"
|
|
inputMode="decimal"
|
|
{...register("basicAmount", { valueAsNumber: true })}
|
|
/>
|
|
{errors.basicAmount && (
|
|
<small className="danger-text">
|
|
{errors.basicAmount.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 col-md-6 mb-2">
|
|
<Label htmlFor="taxAmount" className="form-label" required>
|
|
Tax Amount
|
|
</Label>
|
|
<input
|
|
type="number"
|
|
id="taxAmount"
|
|
className="form-control form-control-sm"
|
|
min="1"
|
|
step="0.01"
|
|
inputMode="decimal"
|
|
{...register("taxAmount", { valueAsNumber: true })}
|
|
/>
|
|
{errors.taxAmount && (
|
|
<small className="danger-text">
|
|
{errors.taxAmount.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
<div className="col-12 my-2 text-start mb-2">
|
|
<div className="col-md-12">
|
|
<Label htmlFor="description" className="form-label" required>
|
|
Description
|
|
</Label>
|
|
<textarea
|
|
id="description"
|
|
className="form-control form-control-sm"
|
|
{...register("description")}
|
|
rows="2"
|
|
></textarea>
|
|
{errors.description && (
|
|
<small className="danger-text">
|
|
{errors.description.message}
|
|
</small>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-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 text-black"
|
|
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
|
|
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 && (
|
|
<div className="d-block">
|
|
{files
|
|
.filter((file) => {
|
|
if (collectionId) {
|
|
return file.isActive;
|
|
}
|
|
return true;
|
|
})
|
|
.map((file, idx) => (
|
|
<a
|
|
key={idx}
|
|
className="d-flex justify-content-between text-start p-1"
|
|
href={file.preSignedUrl || "#"}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<div>
|
|
<span className="mb-0 text-secondary small d-block">
|
|
{file.fileName}
|
|
</span>
|
|
<span className="text-body-secondary small d-block">
|
|
{file.fileSize ? formatFileSize(file.fileSize) : ""}
|
|
</span>
|
|
</div>
|
|
<i
|
|
className="bx bx-trash bx-sm cursor-pointer text-danger"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
removeFile(collectionId ? file.documentId : idx);
|
|
}}
|
|
></i>
|
|
</a>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{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>
|
|
))}
|
|
</div>
|
|
|
|
<div className="d-flex justify-content-end gap-3">
|
|
{" "}
|
|
<button
|
|
type="reset"
|
|
className="btn btn-label-secondary btn-sm mt-3"
|
|
onClick={handleClose}
|
|
disabled={isPending}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary btn-sm mt-3"
|
|
disabled={isPending}
|
|
>
|
|
{isPending ? "Please Wait..." : "Submit"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</FormProvider>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ManageCollection;
|