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:
pramod.mahajan 2025-12-01 04:13:29 +00:00
commit 0482bfe191
10 changed files with 342 additions and 170 deletions

View File

@ -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>

View 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

View File

@ -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>

View File

@ -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

View File

@ -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"

View File

@ -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>
); );
}; };

View File

@ -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",

View File

@ -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>,
}, },
{ {

View File

@ -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={() =>

View 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.
`;