handle this properly update RecurringExpense with new Component UserTag

This commit is contained in:
pramod.mahajan 2025-11-07 12:34:22 +05:30
parent 5f766b4028
commit 5fea95f006
6 changed files with 791 additions and 495 deletions

View File

@ -46,6 +46,8 @@
<link rel="stylesheet" href="/assets/vendor/libs/bs-stepper/bs-stepper.css" /> <link rel="stylesheet" href="/assets/vendor/libs/bs-stepper/bs-stepper.css" />
<link rel="stylesheet" href="/assets/vendor/libs/bootstrap-select/bootstrap-select.css" /> <link rel="stylesheet" href="/assets/vendor/libs/bootstrap-select/bootstrap-select.css" />
<link rel="stylesheet" href="/assets/vendor/libs/select2/select2.css" /> <link rel="stylesheet" href="/assets/vendor/libs/select2/select2.css" />
<link rel="stylesheet" href="/assets/vendor/libs/tagify/tagify.css" />
<link rel="stylesheet" href="/assets/vendor/libs/tagify/tagify.js" />
<link rel="stylesheet" href="/assets/vendor/libs/animate-css/animate.css" /> <link rel="stylesheet" href="/assets/vendor/libs/animate-css/animate.css" />
<link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" /> <link rel="stylesheet" href="/assets/vendor/libs/sweetalert2/sweetalert2.css" />

View File

@ -725,7 +725,8 @@
transition: none; transition: none;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.4231rem !important; /* padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.4231rem !important; */
padding: calc(2px - var(--bs-border-width)) 0.4375rem 0.2rem !important;
} }
.fv-plugins-bootstrap5-row-invalid .tagify.form-control { .fv-plugins-bootstrap5-row-invalid .tagify.form-control {
padding: 0 calc(0.4375rem - var(--bs-border-width)) calc(0.4375rem - 2px) !important; padding: 0 calc(0.4375rem - var(--bs-border-width)) calc(0.4375rem - 2px) !important;

View File

@ -1,15 +1,31 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react";
import Label from '../common/Label'; import Label from "../common/Label";
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from "react-hook-form";
import { useExpenseCategory, useRecurringStatus } from '../../hooks/masterHook/useMaster'; import {
import DatePicker from '../common/DatePicker'; useExpenseCategory,
import { zodResolver } from '@hookform/resolvers/zod'; useRecurringStatus,
import { defaultRecurringExpense, PaymentRecurringExpense } from './RecurringExpenseSchema'; } from "../../hooks/masterHook/useMaster";
import { FREQUENCY_FOR_RECURRING, INR_CURRENCY_CODE } from '../../utils/constants'; import DatePicker from "../common/DatePicker";
import { useCurrencies, useProjectName } from '../../hooks/useProjects'; import { zodResolver } from "@hookform/resolvers/zod";
import { useCreateRecurringExpense, usePayee, useRecurringExpenseDetail, useUpdateRecurringExpense } from '../../hooks/useExpense'; import {
import InputSuggestions from '../common/InputSuggestion'; defaultRecurringExpense,
import MultiEmployeeSearchInput from '../common/MultiEmployeeSearchInput'; PaymentRecurringExpense,
} from "./RecurringExpenseSchema";
import {
FREQUENCY_FOR_RECURRING,
INR_CURRENCY_CODE,
} from "../../utils/constants";
import { useCurrencies, useProjectName } from "../../hooks/useProjects";
import {
useCreateRecurringExpense,
usePayee,
useRecurringExpenseDetail,
useUpdateRecurringExpense,
} from "../../hooks/useExpense";
import InputSuggestions from "../common/InputSuggestion";
import MultiEmployeeSearchInput from "../common/MultiEmployeeSearchInput";
import UsersTagInput from "../common/usesInput";
import { useEmployeesName } from "../../hooks/useEmployees";
function ManageRecurringExpense({ closeModal, requestToEdit = null }) { function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
const { const {
@ -18,15 +34,46 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
isError, isError,
error: requestError, error: requestError,
} = useRecurringExpenseDetail(requestToEdit); } = useRecurringExpenseDetail(requestToEdit);
const { data: employees } = useEmployeesName()
//APIs //APIs
const { projectNames, loading: projectLoading, error, isError: isProjectError, } = useProjectName(); const {
const { data: currencyData, isLoading: currencyLoading, isError: currencyError } = useCurrencies(); projectNames,
const { data: statusData, isLoading: statusLoading, isError: statusError } = useRecurringStatus(); loading: projectLoading,
const { data: Payees, isLoading: isPayeeLoaing, isError: isPayeeError, error: payeeError } = usePayee() error,
const { ExpenseCategories, loading: ExpenseLoading, error: ExpenseError } = useExpenseCategory(); isError: isProjectError,
} = useProjectName();
const {
data: currencyData,
isLoading: currencyLoading,
isError: currencyError,
} = useCurrencies();
const {
data: statusData,
isLoading: statusLoading,
isError: statusError,
} = useRecurringStatus();
const {
data: Payees,
isLoading: isPayeeLoaing,
isError: isPayeeError,
error: payeeError,
} = usePayee();
const {
ExpenseCategories,
loading: ExpenseLoading,
error: ExpenseError,
} = useExpenseCategory();
const schema = PaymentRecurringExpense(); const schema = PaymentRecurringExpense();
const { register, control, watch, handleSubmit, setValue, reset, formState: { errors }, } = useForm({ const {
register,
control,
watch,
handleSubmit,
setValue,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: defaultRecurringExpense, defaultValues: defaultRecurringExpense,
}); });
@ -35,22 +82,29 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
closeModal(); closeModal();
}; };
const { mutate: CreateRecurringExpense, isPending: createPending } = useCreateRecurringExpense( const { mutate: CreateRecurringExpense, isPending: createPending } =
() => { useCreateRecurringExpense(() => {
handleClose(); handleClose();
} });
); const { mutate: RecurringExpenseUpdate, isPending } =
const { mutate: RecurringExpenseUpdate, isPending } = useUpdateRecurringExpense(() => useUpdateRecurringExpense(() => handleClose());
handleClose() const handleEmailGetting = (userArray = []) => {
); if (!Array.isArray(userArray) || userArray.length === 0) return [];
return userArray
.map((empId) => {
const foundUser = employees?.data?.find((user) => user.id === empId);
return foundUser?.email || null;
})
.filter(Boolean).join(",")
};
useEffect(() => { useEffect(() => {
if (requestToEdit && data) { if (requestToEdit && data) {
reset({ reset({
title: data.title || "", title: data.title || "",
description: data.description || "", description: data.description || "",
payee: data.payee || "", payee: data.payee || "",
notifyTo: data.notifyTo || "", notifyTo: data.notifyTo ? data.notifyTo.map((usr)=>usr.id) : [],
currencyId: data.currency.id || "", currencyId: data.currency.id || "",
amount: data.amount || "", amount: data.amount || "",
strikeDate: data.strikeDate?.slice(0, 10) || "", strikeDate: data.strikeDate?.slice(0, 10) || "",
@ -58,20 +112,16 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
paymentBufferDays: data.paymentBufferDays || "", paymentBufferDays: data.paymentBufferDays || "",
numberOfIteration: data.numberOfIteration || "", numberOfIteration: data.numberOfIteration || "",
expenseCategoryId: data.expenseCategory.id || "", expenseCategoryId: data.expenseCategory.id || "",
statusId: data.statusId || "", statusId: data.status.id || "",
frequency: data.frequency || "", frequency: data.frequency || "",
isVariable: data.isVariable || false, isVariable: data.isVariable || false,
}); });
} }
}, [data, reset]); }, [data, reset]);
useEffect(() => { useEffect(() => {
if (!requestToEdit && currencyData && currencyData.length > 0) { if (!requestToEdit && currencyData && currencyData.length > 0) {
const inrCurrency = currencyData.find( const inrCurrency = currencyData.find((c) => c.id === INR_CURRENCY_CODE);
(c) => c.id === INR_CURRENCY_CODE
);
if (inrCurrency) { if (inrCurrency) {
setValue("currencyId", INR_CURRENCY_CODE, { shouldValidate: true }); setValue("currencyId", INR_CURRENCY_CODE, { shouldValidate: true });
} }
@ -81,7 +131,10 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
const onSubmit = (fromdata) => { const onSubmit = (fromdata) => {
let payload = { let payload = {
...fromdata, ...fromdata,
strikeDate: fromdata.strikeDate ? new Date(fromdata.strikeDate).toISOString() : null, strikeDate: fromdata.strikeDate
? new Date(fromdata.strikeDate).toISOString()
: null,
notifyTo:handleEmailGetting(fromdata.notifyTo)
}; };
if (requestToEdit) { if (requestToEdit) {
const editPayload = { ...payload, id: data.id }; const editPayload = { ...payload, id: data.id };
@ -94,7 +147,9 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
return ( return (
<div className="container p-3"> <div className="container p-3">
<h5 className="m-0"> <h5 className="m-0">
{requestToEdit ? "Update Expense Recurring " : "Create Expense Recurring"} {requestToEdit
? "Update Expense Recurring "
: "Create Expense Recurring"}
</h5> </h5>
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}> <form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
{/* Project and Category */} {/* Project and Category */}
@ -167,9 +222,7 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
placeholder="Enter title" placeholder="Enter title"
/> />
{errors.title && ( {errors.title && (
<small className="danger-text"> <small className="danger-text">{errors.title.message}</small>
{errors.title.message}
</small>
)} )}
</div> </div>
@ -211,7 +264,10 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
checked={field.value === true} checked={field.value === true}
onChange={() => field.onChange(true)} onChange={() => field.onChange(true)}
/> />
<Label htmlFor="isVariableTrue" className="form-check-label"> <Label
htmlFor="isVariableTrue"
className="form-check-label"
>
Is Variable Is Variable
</Label> </Label>
</div> </div>
@ -224,7 +280,10 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
checked={field.value === false} checked={field.value === false}
onChange={() => field.onChange(false)} onChange={() => field.onChange(false)}
/> />
<Label htmlFor="isVariableFalse" className="form-check-label"> <Label
htmlFor="isVariableFalse"
className="form-check-label"
>
Fixed Fixed
</Label> </Label>
</div> </div>
@ -247,12 +306,10 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
name="strikeDate" name="strikeDate"
control={control} control={control}
minDate={new Date()} minDate={new Date()}
className='w-100' className="w-100"
/> />
{errors.strikeDate && ( {errors.strikeDate && (
<small className="danger-text"> <small className="danger-text">{errors.strikeDate.message}</small>
{errors.strikeDate.message}
</small>
)} )}
</div> </div>
@ -353,7 +410,9 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
> >
<option value="">Select Status</option> <option value="">Select Status</option>
{statusLoading && <option>Loading...</option>} {statusLoading && <option>Loading...</option>}
{!statusLoading && !statusError && statusData?.map((status) => ( {!statusLoading &&
!statusError &&
statusData?.map((status) => (
<option key={status.id} value={status.id}> <option key={status.id} value={status.id}>
{status.name} {status.name}
</option> </option>
@ -381,7 +440,9 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
placeholder="Enter payment buffer days" placeholder="Enter payment buffer days"
/> />
{errors.paymentBufferDays && ( {errors.paymentBufferDays && (
<small className="danger-text">{errors.paymentBufferDays.message}</small> <small className="danger-text">
{errors.paymentBufferDays.message}
</small>
)} )}
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
@ -398,12 +459,13 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
placeholder="Enter number of iterations" placeholder="Enter number of iterations"
/> />
{errors.numberOfIteration && ( {errors.numberOfIteration && (
<small className="danger-text">{errors.numberOfIteration.message}</small> <small className="danger-text">
{errors.numberOfIteration.message}
</small>
)} )}
</div> </div>
</div> </div>
<div className="row my-2 text-start"> <div className="row my-2 text-start">
<div className="col-md-6"> <div className="col-md-6">
<Label htmlFor="notifyTo" className="form-label" required> <Label htmlFor="notifyTo" className="form-label" required>
@ -417,7 +479,13 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
placeholder="Select Employees" placeholder="Select Employees"
forAll={true} forAll={true}
/> */} /> */}
<UsersTagInput
control={control}
name="notifyTo"
placeholder="Type to search users"
projectId={watch("projectId")}
forAll={true}
/>
</div> </div>
</div> </div>
@ -440,14 +508,6 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
)} )}
</div> </div>
</div> </div>
<div className="col-md-6 mb-6">
<label for="TagifyUserList" class="form-label">Users List</label>
<input
id="TagifyUserList"
name="TagifyUserList"
class="form-control"
value="abatisse2@nih.gov, Justinian Hattersley" />
</div>
<div className="d-flex justify-content-end gap-3"> <div className="d-flex justify-content-end gap-3">
<button <button
@ -457,17 +517,17 @@ function ManageRecurringExpense({ closeModal, requestToEdit = null }) {
> >
Cancel Cancel
</button> </button>
<button <button type="submit" className="btn btn-primary btn-sm mt-3">
type="submit" {createPending || isPending
className="btn btn-primary btn-sm mt-3" ? "Please wait...."
> : requestToEdit
{createPending || isPending ? "Please wait...." : requestToEdit ? "Update":"Submit"} ? "Update"
: "Submit"}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
) );
} }
export default ManageRecurringExpense export default ManageRecurringExpense;

View File

@ -1,12 +1,12 @@
import { boolean, z } from "zod"; import { boolean, z } from "zod";
import { INR_CURRENCY_CODE } from "../../utils/constants"; import { INR_CURRENCY_CODE } from "../../utils/constants";
export const PaymentRecurringExpense = (expenseTypes) => { export const PaymentRecurringExpense = () => {
return z.object({ return z.object({
title: z.string().min(1, { message: "Title is required" }).transform((val) => val.trim()), title: z.string().min(1, { message: "Title is required" }).transform((val) => val.trim()),
description: z.string().min(1, { message: "Description is required" }).transform((val) => val.trim()), description: z.string().min(1, { message: "Description is required" }).transform((val) => val.trim()),
payee: z.string().min(1, { message: "Payee name is required" }).transform((val) => val.trim()), payee: z.string().min(1, { message: "Payee name is required" }).transform((val) => val.trim()),
notifyTo: z.string().min(1, { message: "Notification e-mail is required" }).transform((val) => val.trim()), notifyTo: z.array(z.string()).min(1,"Please select at lest one user"),
currencyId: z currencyId: z
.string() .string()
.min(1, { message: "Currency is required" }) .min(1, { message: "Currency is required" })
@ -77,7 +77,7 @@ export const defaultRecurringExpense = {
title: "", title: "",
description: "", description: "",
payee: "", payee: "",
notifyTo: "", notifyTo: [],
currencyId: "", currencyId: "",
amount: 0, amount: 0,
strikeDate: "", strikeDate: "",

View File

@ -1,87 +1,294 @@
import { useEffect, useRef } from "react"; import { useState, useEffect, useRef, useMemo } from "react";
import { useController } from "react-hook-form"; import { useController } from "react-hook-form";
import { useEmployeesName } from "../../hooks/useEmployees";
import { useDebounce } from "../../utils/appUtils"; import { useDebounce } from "../../utils/appUtils";
import { useEmployeesName } from "../../hooks/useEmployees";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
const UsersTagInput = ({ control, name, projectId, placeholder, forAll }) => { const UsersTagInput = ({
const { field } = useController({ name, control }); control,
name,
placeholder,
projectId,
forAll,
isApplicationUser = false,
}) => {
const {
field: { value = [], onChange },
} = useController({ name, control });
const [search, setSearch] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const [filteredUsers, setFilteredUsers] = useState([]);
const [userCache, setUserCache] = useState({});
const dropdownRef = useRef(null);
const inputRef = useRef(null); const inputRef = useRef(null);
const tagifyRef = useRef(null); const activeIndexRef = useRef(-1);
// debounce the search term const debouncedSearch = useDebounce(search, 300);
const debouncedSearch = useDebounce("", 400); const { data: employees, isLoading } = useEmployeesName(
const { data: employees } = useEmployeesName(projectId, debouncedSearch, forAll); projectId,
debouncedSearch,
forAll
);
// Keep both filtered list and cache updated
useEffect(() => { useEffect(() => {
if (!window.Tagify || !inputRef.current) return; if (employees?.data?.length) {
setFilteredUsers(employees.data);
activeIndexRef.current = -1;
// Initialize Tagify on the input // cache all fetched users by id
const Tagify = window.Tagify; setUserCache((prev) => {
tagifyRef.current = new Tagify(inputRef.current, { const updated = { ...prev };
enforceWhitelist: false, employees.data.forEach((u) => {
dropdown: { updated[u.id] = u;
enabled: 1,
classname: "users-list",
searchKeys: ["value", "email"],
},
templates: {
dropdownItem: (tagData) => `
<div class="tagify__dropdown__item">
<div class="tagify__dropdown__item__avatar-wrap">
<img src="${tagData.avatar || '/default-avatar.png'}" alt="">
</div>
<strong>${tagData.value}</strong>
<span>${tagData.email || ""}</span>
</div>
`,
tag: (tagData) => `
<tag title="${tagData.value}" contenteditable="false" spellcheck="false" class="tagify__tag">
<div>
<span class="tagify__tag__avatar-wrap">
<img src="${tagData.avatar || '/default-avatar.png'}" alt="">
</span>
<span>${tagData.value}</span>
</div>
<x title="remove tag" class="tagify__tag__removeBtn" role="button"></x>
</tag>
`,
},
}); });
return updated;
// Set default value (for editing case)
if (field.value?.length) {
tagifyRef.current.addTags(field.value);
}
// When tagify value changes, update form field
tagifyRef.current.on("change", (e) => {
const newVal = JSON.parse(e.detail.value || "[]");
field.onChange(newVal);
}); });
} else {
return () => tagifyRef.current.destroy(); setFilteredUsers([]);
}, []);
// Update whitelist whenever employees change
useEffect(() => {
if (tagifyRef.current && employees?.data) {
tagifyRef.current.settings.whitelist = employees.data.map((emp) => ({
value: `${emp.firstName} ${emp.lastName}`,
email: emp.email,
avatar: emp.avatarUrl || "",
id: emp.id,
}));
} }
}, [employees]); }, [employees]);
// close dropdown when clicking outside
useEffect(() => {
const onDocClick = (e) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target) &&
!inputRef.current.contains(e.target)
) {
setShowDropdown(false);
}
};
document.addEventListener("mousedown", onDocClick);
return () => document.removeEventListener("mousedown", onDocClick);
}, []);
// select a user
const handleSelect = (user) => {
if (value.includes(user.id)) return;
const updated = [...value, user.id];
onChange(updated);
setSearch("");
setShowDropdown(false);
setTimeout(() => inputRef.current?.focus(), 0);
};
// remove selected user
const handleRemove = (id) => {
const updated = value.filter((uid) => uid !== id);
onChange(updated);
};
// keyboard navigation
const onInputKeyDown = (e) => {
if (!showDropdown) return;
const max = Math.max(0, filteredUsers.length - 1);
if (e.key === "ArrowDown") {
e.preventDefault();
activeIndexRef.current = Math.min(max, activeIndexRef.current + 1);
scrollToActive();
} else if (e.key === "ArrowUp") {
e.preventDefault();
activeIndexRef.current = Math.max(0, activeIndexRef.current - 1);
scrollToActive();
} else if (e.key === "Enter") {
e.preventDefault();
const idx = activeIndexRef.current;
if (idx >= 0 && filteredUsers[idx]) handleSelect(filteredUsers[idx]);
} else if (e.key === "Escape") {
setShowDropdown(false);
}
};
// scroll active dropdown item into view
const scrollToActive = () => {
const wrapper = dropdownRef.current?.querySelector(
".tagify__dropdown__wrapper"
);
const items = wrapper?.querySelectorAll(".tagify__dropdown__item");
const idx = activeIndexRef.current;
if (items && items[idx]) {
const item = items[idx];
const itemTop = item.offsetTop;
const itemBottom = itemTop + item.offsetHeight;
if (wrapper.scrollTop > itemTop) wrapper.scrollTop = itemTop;
else if (wrapper.scrollTop + wrapper.clientHeight < itemBottom)
wrapper.scrollTop = itemBottom - wrapper.clientHeight;
}
};
// resolve user details by ID (for rendering tags)
const resolveUserById = (id) => {
return userCache[id] || filteredUsers.find((u) => u.id === id);
};
// main visible users list (memoized)
const visibleUsers = useMemo(() => {
const baseList = isApplicationUser
? (filteredUsers || []).filter((u) => u?.email)
: filteredUsers || [];
// also include selected users even if missing from current API
const selectedUsers =
Array.isArray(value) && value.length
? value.map((uid) => userCache[uid]).filter(Boolean)
: [];
// merge unique
const merged = [
...selectedUsers,
...baseList.filter((u) => !selectedUsers.some((s) => s.id === u.id)),
];
return merged;
}, [filteredUsers, isApplicationUser, value, userCache]);
return ( return (
<input <div
type="text" className="tagify form-control d-flex align-items-center flex-wrap position-relative "
ref={inputRef} ref={dropdownRef}
placeholder={placeholder || "Select users..."} >
className="form-control form-control-sm" {/* Selected tags (chips) */}
{value.map((id) => {
const u = resolveUserById(id);
if (!u) return null;
return (
<span
key={id}
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
role="listitem"
>
<div className="d-flex align-items-center">
{u.photo ? (
<span className="tagify__tag__avatar-wrap me-1">
<img
src={u.avatarUrl || "/default-avatar.png"}
alt={`${u.firstName || ""} ${u.lastName || ""}`}
style={{ width: 12, height: 12, objectFit: "cover" }}
/> />
</span>
) : (
<div className="avatar avatar-xs me-2">
<span className="avatar-initial rounded-circle bg-label-secondary">
{u.firstName?.[0] || ""}
{u.lastName?.[0] || ""}
</span>
</div>
)}
<div className="d-flex flex-column">
<span className="tagify__tag-text">
{u.firstName} {u.lastName}
</span>
</div>
</div>
<button
type="button"
className="tagify__tag__removeBtn"
onClick={() => handleRemove(id)}
aria-label={`Remove ${u.firstName}`}
title="Remove"
/>
</span>
);
})}
<input
ref={inputRef}
type="text"
value={search}
id="TagifyUserList"
name="TagifyUserList"
className="tagify__input flex-grow-1 border-0 bg-transparent"
placeholder={placeholder || "Type to search users..."}
onChange={(e) => {
setSearch(e.target.value);
setShowDropdown(true);
}}
onFocus={() => {
setShowDropdown(true);
}}
onKeyDown={onInputKeyDown}
autoComplete="off"
aria-expanded={showDropdown}
aria-haspopup="listbox"
/>
{showDropdown && (
<div
className="tagify__dropdown users-list position-absolute w-100 shadow-sm rounded-2"
style={{
zIndex: 1050,
top: "100%",
left: 0,
marginTop: 6,
pointerEvents: "auto",
}}
role="listbox"
>
<div
className="tagify__dropdown__wrapper border rounded-2"
style={{
maxHeight: 200,
overflowY: "auto",
overflowX: "hidden",
scrollbarWidth: "thin",
}}
>
{isLoading ? (
<div className="py-6 px-2 text-center text-muted small">
Loading...
</div>
) : filteredUsers.length === 0 ? (
<div className="py-6 px-2 text-center text-muted small">
No users found
</div>
) : (
filteredUsers.map((user, idx) => {
const isActive = idx === activeIndexRef.current;
return (
<div
key={user.id}
role="option"
aria-selected={isActive}
tabIndex={0}
className={`tagify__dropdown__item ${
isActive ? "tagify__dropdown__item--active" : ""
}`}
onMouseEnter={() => (activeIndexRef.current = idx)}
onMouseDown={(e) => {
e.preventDefault();
handleSelect(user);
}}
>
<div className="d-flex flex-row gap-2">
{user.photo ? (
<img
src={user.photo || "/default-avatar.png"}
alt={`${user.firstName || ""} ${user.lastName || ""}`}
/>
) : (
<Avatar
size="xs"
firstName={user.firstName}
lastName={user.lastName}
/>
)}
<strong>
{user.firstName} {user.lastName}
</strong>
</div>
</div>
);
})
)}
</div>
</div>
)}
</div>
); );
}; };

View File

@ -13,6 +13,7 @@ import Label from "../../components/common/Label";
import AdvancePaymentList from "../../components/AdvancePayment/AdvancePaymentList"; import AdvancePaymentList from "../../components/AdvancePayment/AdvancePaymentList";
import { employee } from "../../data/masters"; import { employee } from "../../data/masters";
import { formatFigure } from "../../utils/appUtils"; import { formatFigure } from "../../utils/appUtils";
import UsersTagInput from "../../components/common/usesInput";
export const AdvancePaymentContext = createContext(); export const AdvancePaymentContext = createContext();
export const useAdvancePaymentContext = () => { export const useAdvancePaymentContext = () => {
@ -26,7 +27,7 @@ export const useAdvancePaymentContext = () => {
}; };
const AdvancePaymentPage = () => { const AdvancePaymentPage = () => {
const [balance, setBalance] = useState(null); const [balance, setBalance] = useState(null);
const { control, reset, watch } = useForm({ const { reset, watch } = useForm({
defaultValues: { defaultValues: {
employeeId: "", employeeId: "",
}, },
@ -39,6 +40,16 @@ const AdvancePaymentPage = () => {
employeeId: selectedEmpoyee || "", employeeId: selectedEmpoyee || "",
}); });
}, [reset]); }, [reset]);
const { control, handleSubmit } = useForm({
defaultValues: {
selectedUsers: ["08ddb487-9c34-4295-857b-7017549c4d8c"], // works on update
},
});
const onSubmit = (data) => {
console.log("Selected User IDs:", data.selectedUsers);
};
return ( return (
<AdvancePaymentContext.Provider value={{ setBalance }}> <AdvancePaymentContext.Provider value={{ setBalance }}>
<div className="container-fluid"> <div className="container-fluid">
@ -83,6 +94,21 @@ const AdvancePaymentPage = () => {
</div> </div>
</div> </div>
<AdvancePaymentList employeeId={selectedEmployeeId} /> <AdvancePaymentList employeeId={selectedEmployeeId} />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="col-6">
<label className="form-label fw-semibold">Assign Users</label>
<UsersTagInput
control={control}
name="selectedUsers"
placeholder="Type to search users"
forAll={true}
/>
</div>
<button type="submit" className="btn btn-primary mt-3">
Submit
</button>
</form>
</div> </div>
</div> </div>
</AdvancePaymentContext.Provider> </AdvancePaymentContext.Provider>