Adding Dropdown in Employee.

This commit is contained in:
Kartik Sharma 2025-12-06 10:11:58 +05:30
parent dbf4f5e9c8
commit 0e75a3e1c9
3 changed files with 298 additions and 278 deletions

View File

@ -17,6 +17,8 @@ import {
import showToast from "../../services/toastService"; import showToast from "../../services/toastService";
import { useDocumentContext } from "./Documents"; import { useDocumentContext } from "./Documents";
import { isPending } from "@reduxjs/toolkit"; import { isPending } from "@reduxjs/toolkit";
import { AppFormController, AppFormProvider } from "../../hooks/appHooks/useAppForm";
import SelectField from "../common/Forms/SelectField";
const toBase64 = (file) => const toBase64 = (file) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@ -72,9 +74,12 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
handleSubmit, handleSubmit,
watch, watch,
setValue, setValue,
control,
reset, reset,
formState: { errors }, formState: { errors },
} = methods; } = methods;
const { mutate: UploadDocument, isPending: isUploading } = useUploadDocument( const { mutate: UploadDocument, isPending: isUploading } = useUploadDocument(
() => { () => {
showToast("Document Uploaded Successfully", "success"); showToast("Document Uploaded Successfully", "success");
@ -88,33 +93,33 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
} }
); );
const onSubmit = (data) => { const onSubmit = (data) => {
const normalizeAttachment = (attachment) => { const normalizeAttachment = (attachment) => {
if (!attachment) return null; if (!attachment) return null;
return { return {
...attachment, ...attachment,
fileSize: Math.ceil(attachment.fileSize / 1024), fileSize: Math.ceil(attachment.fileSize / 1024),
};
}; };
};
const payload = { const payload = {
...data, ...data,
attachment: normalizeAttachment(data.attachment), attachment: normalizeAttachment(data.attachment),
};
if (ManageDoc?.document) {
const DocumentPayload = {
...payload,
id: DocData.id,
tags: MergedTagsWithExistenStatus(data?.tags, DocData?.tags),
}; };
UpdateDocument({ documentId: DocData?.id, DocumentPayload });
} else { if (ManageDoc?.document) {
const DocumentPayload = { ...payload, entityId: Entity }; const DocumentPayload = {
UploadDocument(DocumentPayload); ...payload,
} id: DocData.id,
}; tags: MergedTagsWithExistenStatus(data?.tags, DocData?.tags),
};
UpdateDocument({ documentId: DocData?.id, DocumentPayload });
} else {
const DocumentPayload = { ...payload, entityId: Entity };
UploadDocument(DocumentPayload);
}
};
const { const {
data: DocData, data: DocData,
@ -134,7 +139,7 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
const { DocumentTypes, isLoading: isTypeLoading } = useDocumentTypes( const { DocumentTypes, isLoading: isTypeLoading } = useDocumentTypes(
categoryId || null categoryId || null
); );
const {data:DocumentTags} = useDocumentTags() const { data: DocumentTags } = useDocumentTags()
// Update schema whenever document type changes // Update schema whenever document type changes
useEffect(() => { useEffect(() => {
@ -144,7 +149,7 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
(t) => String(t.id) === String(documentTypeId) (t) => String(t.id) === String(documentTypeId)
); );
if (!type) return; if (!type) return;
setSelectedType(type) setSelectedType(type)
const newSchema = DocumentPayloadSchema({ const newSchema = DocumentPayloadSchema({
isMandatory: type.isMandatory ?? false, isMandatory: type.isMandatory ?? false,
regexExpression: type.regexExpression ?? null, regexExpression: type.regexExpression ?? null,
@ -200,10 +205,10 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
t === "application/pdf" t === "application/pdf"
? ".pdf" ? ".pdf"
: t === "image/jpeg" : t === "image/jpeg"
? ".jpg,.jpeg" ? ".jpg,.jpeg"
: t === "image/png" : t === "image/png"
? ".png" ? ".png"
: "" : ""
) )
.join(",") || ""; .join(",") || "";
@ -231,200 +236,209 @@ const ManageDocument = ({ closeModal, Document_Entity, Entity }) => {
const isPending = isUploading || isUpdating; const isPending = isUploading || isUpdating;
return ( return (
<div className="p-2"> <AppFormProvider {...methods}>
<p className="fw-bold fs-6">Upload New Document</p> <div className="p-2">
<FormProvider key={documentTypeId} {...methods}> <h5 className="">Upload New Document</h5>
<form onSubmit={handleSubmit(onSubmit)} className="text-start"> <FormProvider key={documentTypeId} {...methods}>
{/* Category */} <form onSubmit={handleSubmit(onSubmit)} className="text-start">
<div className="mb-2"> {/* Category */}
<Label htmlFor="documentCategoryId" required>Document Category</Label> <div className="col-12 col-md-12 mb-2 mb-md-4">
<select <AppFormController
{...register("documentCategoryId")} name="documentCategoryId"
className="form-select form-select-sm" control={control}
> render={({ field }) => (
{isLoading && ( <SelectField
<option disabled value=""> label="Document Category"
Loading... options={DocumentCategories ?? []}
</option> placeholder="Select Category"
)} required
{!isLoading && <option value="">Select Category</option>} labelKeyKey="name"
{DocumentCategories?.map((type) => ( valueKeyKey="id"
<option key={type.id} value={type.id}> value={field.value}
{type.name} onChange={field.onChange}
</option> isLoading={isLoading}
))} className="m-0"
</select> />
{errors.documentCategoryId && (
<div className="danger-text">
{errors.documentCategoryId.message}
</div>
)}
</div>
{/* Type */}
{categoryId && (
<div className="mb-2">
<Label htmlFor="documentTypeId" required>Document Type</Label>
<select
{...register("documentTypeId")}
className="form-select form-select-sm"
>
{isTypeLoading && (
<option disabled value="">
Loading...
</option>
)} )}
{DocumentTypes?.map((type) => ( />
<option key={type.id} value={type.id}>
{type.name} {errors.documentCategoryId && (
</option> <small className="danger-text">
))} {errors.documentCategoryId.message}
</select> </small>
{errors.documentTypeId && (
<div className="danger-text">
{errors.documentTypeId.message}
</div>
)} )}
</div> </div>
)}
{/* Document ID */}
<div className="mb-2">
<label
htmlFor="documentId"
required={selectedType?.isMandatory ?? false}
>
Document ID
</label>
<input
type="text"
className="form-control form-control-sm"
{...register("documentId")}
/>
{errors.documentId && (
<div className="danger-text">{errors.documentId.message}</div>
)}
</div>
{/* Document Name */} {/* Type */}
<div className="mb-2"> {categoryId && (
<Label htmlFor="name" required> <div className="col-12 col-md-12 mb-2 mb-md-4">
Document Name <AppFormController
</Label> name="documentTypeId"
<input control={control}
type="text" render={({ field }) => (
className="form-control form-control-sm" <SelectField
{...register("name")} label="Document Type"
/> options={DocumentTypes ?? []}
{errors.name && ( placeholder="Select Document Type"
<div className="danger-text">{errors.name.message}</div> required
)} labelKeyKey="name"
</div> valueKeyKey="id"
value={field.value}
onChange={field.onChange}
isLoading={isTypeLoading}
{/* Upload */} className="m-0"
<div className="row my-2"> />
<div className="col-md-12"> )}
<Label htmlFor="attachment" required>Upload Document</Label>
<div
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
style={{ cursor: "pointer" }}
onClick={() => document.getElementById("attachment").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">
({selectedType?.allowedContentType || "PDF/JPG/PNG"}, max{" "}
{selectedType?.maxSizeAllowedInMB ?? 25}MB)
</small>
<input
type="file"
id="attachment"
accept={selectedType?.allowedContentType}
style={{ display: "none" }}
onChange={(e) => {
onFileChange(e);
e.target.value = ""; // reset input
}}
/> />
{errors.documentTypeId && (
<small className="danger-text">
{errors.documentTypeId.message}
</small>
)}
</div> </div>
{errors.attachment && ( )}
<small className="danger-text">
{errors.attachment.message
? errors.attachment.message {/* Document ID */}
: errors.attachment.fileName?.message || <div className="mb-4">
<label
htmlFor="documentId"
required={selectedType?.isMandatory ?? false}
>
Document ID
</label>
<input
type="text"
className="form-control "
{...register("documentId")}
/>
{errors.documentId && (
<div className="danger-text">{errors.documentId.message}</div>
)}
</div>
{/* Document Name */}
<div className="mb-2">
<Label htmlFor="name" required>
Document Name
</Label>
<input
type="text"
className="form-control "
{...register("name")}
/>
{errors.name && (
<div className="danger-text">{errors.name.message}</div>
)}
</div>
{/* Upload */}
<div className="row my-4">
<div className="col-md-12">
<Label htmlFor="attachment" required>Upload Document</Label>
<div
className="border border-secondary border-dashed rounded p-4 text-center bg-textMuted position-relative"
style={{ cursor: "pointer" }}
onClick={() => document.getElementById("attachment").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">
({selectedType?.allowedContentType || "PDF/JPG/PNG"}, max{" "}
{selectedType?.maxSizeAllowedInMB ?? 25}MB)
</small>
<input
type="file"
id="attachment"
accept={selectedType?.allowedContentType}
style={{ display: "none" }}
onChange={(e) => {
onFileChange(e);
e.target.value = ""; // reset input
}}
/>
</div>
{errors.attachment && (
<small className="danger-text">
{errors.attachment.message
? errors.attachment.message
: errors.attachment.fileName?.message ||
errors.attachment.base64Data?.message || errors.attachment.base64Data?.message ||
errors.attachment.contentType?.message || errors.attachment.contentType?.message ||
errors.attachment.fileSize?.message} errors.attachment.fileSize?.message}
</small> </small>
)} )}
{file?.base64Data && ( {file?.base64Data && (
<div className="d-flex justify-content-between text-start p-1 mt-2"> <div className="d-flex justify-content-between text-start p-1 mt-2">
<div> <div>
<span className="mb-0 text-secondary small d-block"> <span className="mb-0 text-secondary small d-block">
{file.fileName} {file.fileName}
</span> </span>
<span className="text-body-secondary small d-block"> <span className="text-body-secondary small d-block">
{(file.fileSize / 1024).toFixed(1)} KB {(file.fileSize / 1024).toFixed(1)} KB
</span> </span>
</div>
<i
className="bx bx-trash bx-sm cursor-pointer text-danger"
onClick={removeFile}
></i>
</div> </div>
<i )}
className="bx bx-trash bx-sm cursor-pointer text-danger" </div>
onClick={removeFile} </div>
></i> <div className="mb-4">
</div> <TagInput name="tags" label="Tags" placeholder="Tags.." options={DocumentTags} />
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
)} )}
</div> </div>
</div>
<div className="mb-2">
<TagInput name="tags" label="Tags" placeholder="Tags.." options={DocumentTags} />
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
)}
</div>
{/* Description */} {/* Description */}
<div className="mb-2"> <div className="mb-4">
<Label htmlFor="description" required>Description</Label> <Label htmlFor="description" required>Description</Label>
<textarea <textarea
rows="2" rows="2"
className="form-control" className="form-control"
{...register("description")} {...register("description")}
></textarea> ></textarea>
{errors.description && ( {errors.description && (
<div className="danger-text">{errors.description.message}</div> <div className="danger-text">{errors.description.message}</div>
)} )}
</div> </div>
{/* Buttons */} {/* Buttons */}
<div className="d-flex justify-content-end gap-3 mt-4"> <div className="d-flex justify-content-end gap-3 mt-4">
<button <button
type="reset" type="reset"
className="btn btn-label-secondary btn-sm" className="btn btn-label-secondary btn-sm"
disabled={isPending} disabled={isPending}
onClick={closeModal} onClick={closeModal}
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="btn btn-primary btn-sm" className="btn btn-primary btn-sm"
disabled={isPending} disabled={isPending}
> >
{isPending ? "Please Wait..." : " Submit"} {isPending ? "Please Wait..." : " Submit"}
</button> </button>
</div> </div>
</form> </form>
</FormProvider> </FormProvider>
</div> </div>
</AppFormProvider>
); );
}; };

View File

@ -517,39 +517,35 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
</div> </div>
</div> </div>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-sm-4"> <div className="col-sm-4 text-start">
<Label className="form-text text-start" required> <AppFormController
Official Designation name="jobRoleId"
</Label> control={control}
<div className="input-group"> render={({ field }) => (
<select <SelectField
className="form-select" label="Official Designation"
{...register("jobRoleId")} required
id="jobRoleId" options={[...job_role].sort((a, b) =>
aria-label="" a?.name?.localeCompare(b?.name)
> )}
<option disabled value=""> placeholder="Select Role"
Select Role labelKeyKey="name"
</option> valueKeyKey="id"
{[...job_role] value={field.value}
.sort((a, b) => a?.name?.localeCompare(b.name)) onChange={field.onChange}
.map((item) => ( className="m-0"
<option value={item?.id} key={item.id}> />
{item?.name}{" "} )}
</option> />
))}
</select>
</div>
{errors.jobRoleId && ( {errors.jobRoleId && (
<div <div className="danger-text text-start" style={{ fontSize: "12px" }}>
className="danger-text text-start"
style={{ fontSize: "12px" }}
>
{errors.jobRoleId.message} {errors.jobRoleId.message}
</div> </div>
)} )}
</div> </div>
<div className="col-sm-4">
<div className="col-sm-4 mt-n1">
<Label className="form-text text-start" required> <Label className="form-text text-start" required>
Emergency Contact Person Emergency Contact Person
</Label> </Label>
@ -570,7 +566,7 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
</div> </div>
)} )}
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4 mt-n1">
<Label className="form-text text-start" required> <Label className="form-text text-start" required>
Emergency Phone Number Emergency Phone Number
</Label> </Label>

View File

@ -10,6 +10,8 @@ import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import showToast from "../../services/toastService"; import showToast from "../../services/toastService";
import { AppFormController } from "../../hooks/appHooks/useAppForm";
import SelectField from "../common/Forms/SelectField";
export const ProjectPermissionSchema = z.object({ export const ProjectPermissionSchema = z.object({
employeeId: z.string().min(1, "Employee is required"), employeeId: z.string().min(1, "Employee is required"),
@ -46,26 +48,26 @@ const ProjectPermission = () => {
); );
useEffect(() => { useEffect(() => {
if (!selectedEmployee) return; if (!selectedEmployee) return;
const enabledPerms = const enabledPerms =
selectedEmpPermissions?.permissions
?.filter((perm) => perm.isEnabled)
?.map((perm) => perm.id) || [];
setValue("selectedPermissions", enabledPerms, { shouldValidate: true });
}, [selectedEmpPermissions, setValue, selectedEmployee]);
const selectedPermissions = watch("selectedPermissions") || [];
const existingEnabledIds =
selectedEmpPermissions?.permissions selectedEmpPermissions?.permissions
?.filter((perm) => perm.isEnabled) ?.filter((p) => p.isEnabled)
?.map((perm) => perm.id) || []; ?.map((p) => p.id) || [];
setValue("selectedPermissions", enabledPerms, { shouldValidate: true }); const hasChanges =
}, [selectedEmpPermissions, setValue, selectedEmployee]); selectedPermissions.length !== existingEnabledIds.length ||
selectedPermissions.some((id) => !existingEnabledIds.includes(id));
const selectedPermissions = watch("selectedPermissions") || [];
const existingEnabledIds =
selectedEmpPermissions?.permissions
?.filter((p) => p.isEnabled)
?.map((p) => p.id) || [];
const hasChanges =
selectedPermissions.length !== existingEnabledIds.length ||
selectedPermissions.some((id) => !existingEnabledIds.includes(id));
const { mutate: updatePermission, isPending } = const { mutate: updatePermission, isPending } =
useUpdateProjectLevelEmployeePermission(); useUpdateProjectLevelEmployeePermission();
@ -115,35 +117,42 @@ const hasChanges =
<form className="row" onSubmit={handleSubmit(onSubmit)}> <form className="row" onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex justify-content-between align-items-end gap-2 mb-3"> <div className="d-flex justify-content-between align-items-end gap-2 mb-3">
<div className="text-start d-flex align-items-center gap-2"> <div className="text-start d-flex align-items-center gap-2">
<div className="d-block"> {/* <div className="d-block">
<label className="form-label">Select Employee</label> <label className="form-label">Select Employee</label>
</div> </div> */}
<div className="d-block">
{" "} <div className="d-block flex-grow-1">
<select <AppFormController
className="form-select form-select-sm" name="employeeId"
{...register("employeeId")} control={control}
disabled={isPending} render={({ field }) => (
> <SelectField
{loading ? ( label="Select Employee"
<option value="">Loading...</option> options={
) : ( employees
<> ?.sort((a, b) =>
<option value="">-- Select Employee --</option> `${a?.firstName} ${a?.lastName}`.localeCompare(
{[...employees] `${b?.firstName} ${b?.lastName}`
?.sort((a, b) => )
`${a?.firstName} ${a?.firstName}`?.localeCompare(
`${b?.firstName} ${b?.lastName}`
) )
) ?.map((emp) => ({
?.map((emp) => ( id: emp.id,
<option key={emp.id} value={emp.id}> name: `${emp.firstName} ${emp.lastName}`,
{emp.firstName} {emp.lastName} })) ?? []
</option> }
))} placeholder="-- Select Employee --"
</> required
labelKeyKey="name"
valueKeyKey="id"
value={field.value}
onChange={field.onChange}
isLoading={loading}
disabled={isPending}
className="m-0"
/>
)} )}
</select> />
{errors.employeeId && ( {errors.employeeId && (
<div className="d-block text-danger small"> <div className="d-block text-danger small">
{errors.employeeId.message} {errors.employeeId.message}
@ -152,6 +161,7 @@ const hasChanges =
</div> </div>
</div> </div>
<div className="mt-3 text-end"> <div className="mt-3 text-end">
{hasChanges && ( {hasChanges && (
<button <button