Creating a new component for Document Manager.
This commit is contained in:
parent
1772a2fcd1
commit
7490b805b6
281
src/components/Employee/DocumentForm.jsx
Normal file
281
src/components/Employee/DocumentForm.jsx
Normal file
@ -0,0 +1,281 @@
|
||||
// DocumentForm.js
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
// Helper function to format file size
|
||||
// const formatFileSize = (bytes) => {
|
||||
// if (bytes === 0) return "0 Bytes";
|
||||
// const k = 1024;
|
||||
// const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
// const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
// return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
// };
|
||||
|
||||
// Define the Zod schema for the form
|
||||
const DocumentSchema = z.object({
|
||||
name: z.string().min(1, { message: "Document Name is required." }),
|
||||
documentNumber: z.string().min(1, { message: "Document Number is required." }),
|
||||
category: z.enum(["public", "private"], {
|
||||
errorMap: () => ({ message: "Category is required." }),
|
||||
}),
|
||||
documentType: z.string().min(1, { message: "Document Type is required." }),
|
||||
files: z
|
||||
.array(z.any())
|
||||
.min(1, { message: "At least one file must be uploaded." })
|
||||
.refine(
|
||||
(files) => files.every((file) => file.fileSize <= 5 * 1024 * 1024),
|
||||
{
|
||||
message: "File size exceeds 5MB.",
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
const DocumentForm = ({ initialData, onSave, onCancel }) => {
|
||||
const formKey = initialData ? initialData.name : "new";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
documentNumber: "",
|
||||
category: "",
|
||||
documentType: "",
|
||||
files: initialData ? initialData.files || [] : [],
|
||||
...initialData,
|
||||
});
|
||||
|
||||
const [files, setFiles] = useState(initialData?.files || []);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData && initialData.files) {
|
||||
setFiles(initialData.files);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const onFileChange = (e) => {
|
||||
const newFiles = Array.from(e.target.files).map((file) => ({
|
||||
file,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
preSignedUrl: URL.createObjectURL(file),
|
||||
}));
|
||||
|
||||
setFiles((prevFiles) => [...prevFiles, ...newFiles]);
|
||||
};
|
||||
|
||||
const removeFile = (fileToRemove) => {
|
||||
setFiles((prevFiles) => prevFiles.filter((file) => file !== fileToRemove));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const validationData = {
|
||||
...formData,
|
||||
files,
|
||||
};
|
||||
|
||||
const result = DocumentSchema.safeParse(validationData);
|
||||
|
||||
if (result.success) {
|
||||
setErrors({});
|
||||
onSave({ ...formData, files });
|
||||
} else {
|
||||
const fieldErrors = result.error.flatten().fieldErrors;
|
||||
const fileErrors = fieldErrors.files ? [{ message: fieldErrors.files.join(", ") }] : [];
|
||||
setErrors({
|
||||
...fieldErrors,
|
||||
billAttachments: fileErrors,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={formKey} className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title text-start">
|
||||
{initialData ? "Edit Document" : "Create Document"}
|
||||
</h5>
|
||||
<button type="button" className="btn-close" onClick={onCancel}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Document Name */}
|
||||
<div className="mb-3 text-start">
|
||||
<label htmlFor="name" className="form-label ">
|
||||
Document Name <span className="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errors.name && (
|
||||
<div className="invalid-feedback d-block">{errors.name}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document Number */}
|
||||
<div className="mb-3 text-start">
|
||||
<label htmlFor="documentNumber" className="form-label ">
|
||||
Document Number <span className="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="documentNumber"
|
||||
name="documentNumber"
|
||||
value={formData.documentNumber}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errors.documentNumber && (
|
||||
<div className="invalid-feedback d-block">{errors.documentNumber}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Category Dropdown */}
|
||||
<div className="mb-3 text-start">
|
||||
<label htmlFor="category" className="form-label ">
|
||||
Category <span className="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="category"
|
||||
name="category"
|
||||
value={formData.category}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">Select Category</option>
|
||||
<option value="public">Public</option>
|
||||
<option value="private">Private</option>
|
||||
</select>
|
||||
{errors.category && (
|
||||
<div className="invalid-feedback d-block">{errors.category}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document Type Dropdown */}
|
||||
<div className="mb-3 text-start">
|
||||
<label htmlFor="documentType" className="form-label ">
|
||||
Document Type <span className="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="documentType"
|
||||
name="documentType"
|
||||
value={formData.documentType}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">Select Document Type</option>
|
||||
<option value="Aadhar Card">Aadhar Card</option>
|
||||
<option value="Pan Card">Pan Card</option>
|
||||
<option value="Driving License">Driving License</option>
|
||||
<option value="Passport">Passport</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
{errors.documentType && (
|
||||
<div className="invalid-feedback d-block">{errors.documentType}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File Uploader */}
|
||||
<div className="row my-2 text-start">
|
||||
<div className="col-md-12">
|
||||
<label className="form-label ">
|
||||
Upload Document <span className="text-danger">*</span>
|
||||
</label>
|
||||
<div
|
||||
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() =>
|
||||
document.getElementById("billAttachments").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="billAttachments"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
onChange={onFileChange}
|
||||
onClick={(e) => (e.target.value = null)}
|
||||
/>
|
||||
</div>
|
||||
{errors.billAttachments && (
|
||||
<div className="text-danger small mt-1">
|
||||
{errors.billAttachments.map((err, idx) => (
|
||||
<div key={idx}>{err.message || err.fileSize?.message}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<div className="d-block mt-2">
|
||||
{files.map((file, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
className="d-flex justify-content-between text-start p-1 border-bottom"
|
||||
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">
|
||||
{/* {formatFileSize(file.fileSize)} */}
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
className="bx bx-trash bx-sm cursor-pointer text-danger"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeFile(file);
|
||||
}}
|
||||
></i>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="d-flex justify-content-end gap-2 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary btn-sm">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentForm;
|
@ -1,11 +1,240 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ComingSoonPage } from "../../pages/Misc/ComingSoonPage";
|
||||
// DocumentManager.js
|
||||
import React, { useState } from "react";
|
||||
import DocumentForm from "./DocumentForm";
|
||||
import ConfirmModal from "../common/ConfirmModal";
|
||||
|
||||
const EmpDocuments = () => {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [documents, setDocuments] = useState([
|
||||
{
|
||||
name: "Adhar Card",
|
||||
uploadedDate: "21 Aug, 2025",
|
||||
uploadedBy: "Alice Johnson",
|
||||
fileType: "PDF",
|
||||
url: "#",
|
||||
initials: "AC",
|
||||
files: [],
|
||||
},
|
||||
{
|
||||
name: "Pan Card",
|
||||
uploadedDate: "15 Jul, 2025",
|
||||
uploadedBy: "Bob Smith",
|
||||
fileType: "Excel",
|
||||
url: "#",
|
||||
initials: "PC",
|
||||
files: [],
|
||||
},
|
||||
{
|
||||
name: "Driving Licence",
|
||||
uploadedDate: "10 Aug, 2025",
|
||||
uploadedBy: "Carol Lee",
|
||||
fileType: "Word",
|
||||
url: "#",
|
||||
initials: "DL",
|
||||
files: [],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
uploadedDate: "05 Aug, 2025",
|
||||
uploadedBy: "David Kim",
|
||||
fileType: "PNG",
|
||||
url: "#",
|
||||
initials: "DM",
|
||||
files: [],
|
||||
},
|
||||
{
|
||||
name: "Client Presentation",
|
||||
uploadedDate: "18 Aug, 2025",
|
||||
uploadedBy: "Eve Brown",
|
||||
fileType: "PPT",
|
||||
url: "#",
|
||||
initials: "CP",
|
||||
files: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [currentDocument, setCurrentDocument] = useState(null);
|
||||
|
||||
// New state for the delete modal
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [documentToDelete, setDocumentToDelete] = useState(null);
|
||||
|
||||
const filteredDocuments = documents.filter((doc) =>
|
||||
(doc.name || "Unnamed Document").toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setSearchText(e.target.value);
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
setCurrentDocument(null);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleEditClick = (doc) => {
|
||||
setCurrentDocument(doc);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleSave = (newDoc) => {
|
||||
if (currentDocument) {
|
||||
// Edit an existing document
|
||||
setDocuments(
|
||||
documents.map((doc) => (doc === currentDocument ? { ...newDoc, initials: doc.initials } : doc))
|
||||
);
|
||||
} else {
|
||||
// Create a new document
|
||||
setDocuments([...documents, { ...newDoc, initials: newDoc.name ? newDoc.name.slice(0, 2).toUpperCase() : "--" }]);
|
||||
}
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setShowForm(false);
|
||||
setCurrentDocument(null);
|
||||
};
|
||||
|
||||
const handleDelete = (doc) => {
|
||||
// Set the document to be deleted and open the modal
|
||||
setDocumentToDelete(doc);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
// New function to handle the confirmed deletion
|
||||
const handleConfirmDelete = () => {
|
||||
if (documentToDelete) {
|
||||
setDocuments(documents.filter((doc) => doc !== documentToDelete));
|
||||
setIsDeleteModalOpen(false); // Close the modal
|
||||
setDocumentToDelete(null); // Clear the document to delete
|
||||
}
|
||||
};
|
||||
|
||||
// Function to close the delete modal
|
||||
const handleCloseDeleteModal = () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setDocumentToDelete(null);
|
||||
};
|
||||
|
||||
const EmpDocuments = ({ profile, loggedInUser }) => {
|
||||
return (
|
||||
<>
|
||||
<ComingSoonPage/>
|
||||
</>
|
||||
<div className="card px-4 mt-2 py-2" style={{ minHeight: "200px" }}>
|
||||
{/* Header: Search & Create */}
|
||||
<div className="d-flex justify-content-between align-items-center py-2">
|
||||
<div className="dataTables_filter">
|
||||
<label className="mb-0">
|
||||
<input
|
||||
type="search"
|
||||
value={searchText}
|
||||
onChange={handleSearch}
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Search Document"
|
||||
aria-controls="DataTables_Table_0"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-primary btn-sm" onClick={handleCreateClick}>
|
||||
Create Document
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-responsive text-nowrap">
|
||||
<table className="table mb-0">
|
||||
<thead>
|
||||
<tr className="text-center">
|
||||
<th colSpan={2}>Document Name</th>
|
||||
<th>Uploaded Date</th>
|
||||
<th>Uploaded By</th>
|
||||
<th>File Type</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredDocuments.length > 0 ? (
|
||||
filteredDocuments.map((doc, index) => (
|
||||
<tr key={index} className="align-middle" style={{ height: "50px" }}>
|
||||
<td colSpan={2}>
|
||||
<div className="d-flex align-items-center">
|
||||
<div
|
||||
className="avatar bg-secondary text-white rounded-circle d-flex align-items-center justify-content-center"
|
||||
style={{ width: "30px", height: "30px", fontSize: "0.75rem" }}
|
||||
>
|
||||
{doc.initials || (doc.name ? doc.name.slice(0, 2).toUpperCase() : "--")}
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column ms-3">
|
||||
<a href={doc.url || "#"} className="text-heading text-truncate">
|
||||
<span className="fw-normal">
|
||||
{doc.name || "Unnamed Document"}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{doc.uploadedDate}</td>
|
||||
<td>{doc.uploadedBy}</td>
|
||||
<td>{doc.fileType}</td>
|
||||
<td className="text-center">
|
||||
<i
|
||||
className="bx bx-edit text-secondary cursor-pointer me-2"
|
||||
onClick={() => handleEditClick(doc)}
|
||||
></i>
|
||||
<i
|
||||
className="bx bx-trash text-danger cursor-pointer"
|
||||
onClick={() => handleDelete(doc)}
|
||||
></i>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center text-muted py-3">
|
||||
No documents available.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Document Form Modal */}
|
||||
{showForm && (
|
||||
<div className="modal fade show d-block" style={{ backgroundColor: "rgba(0, 0, 0, 0.5)" }}>
|
||||
<DocumentForm
|
||||
initialData={currentDocument}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{isDeleteModalOpen && (
|
||||
<div
|
||||
className={`modal fade ${isDeleteModalOpen ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: "block",
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type={"delete"}
|
||||
header={"Delete Document"}
|
||||
message={`Are you sure you want to delete "${documentToDelete?.name || "Unnamed Document"}"?`}
|
||||
onSubmit={handleConfirmDelete}
|
||||
onClose={handleCloseDeleteModal}
|
||||
// `loading` prop is not needed for this example but can be added
|
||||
// `paramData` is also not needed since we're using a state variable
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -64,6 +64,19 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "document" ? "active" : ""} fs-6`}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault(); // Prevent page reload
|
||||
onPillClick("document");
|
||||
}}
|
||||
>
|
||||
<i className="bx bxs-file-doc bx-sm me-2"></i>
|
||||
<span className="d-none d-md-inline">Document</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${
|
||||
|
@ -26,6 +26,7 @@ import ProjectProgressChart from "../../components/Dashboard/ProjectProgressChar
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import AttendanceOverview from "../../components/Dashboard/AttendanceChart";
|
||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||
import EmpDocuments from "../../components/Employee/EmpDocuments";
|
||||
|
||||
const ProjectDetails = () => {
|
||||
|
||||
@ -118,6 +119,10 @@ const ProjectDetails = () => {
|
||||
<Directory IsPage={false} prefernceContacts={projects_Details.id} />
|
||||
</div>
|
||||
);
|
||||
case "document":
|
||||
return (
|
||||
<EmpDocuments IsPage={false} prefernceContacts={projects_Details.id} />
|
||||
);
|
||||
|
||||
default:
|
||||
return <ComingSoonPage />;
|
||||
|
Loading…
x
Reference in New Issue
Block a user