|
No Recurring Expense Found
diff --git a/src/components/RecurringExpense/RecurringExpenseSchema.js b/src/components/RecurringExpense/RecurringExpenseSchema.js
index da62b341..2555a9ac 100644
--- a/src/components/RecurringExpense/RecurringExpenseSchema.js
+++ b/src/components/RecurringExpense/RecurringExpenseSchema.js
@@ -1,12 +1,12 @@
import { boolean, z } from "zod";
import { INR_CURRENCY_CODE } from "../../utils/constants";
-export const PaymentRecurringExpense = (expenseTypes) => {
+export const PaymentRecurringExpense = () => {
return z.object({
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()),
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
.string()
.min(1, { message: "Currency is required" })
@@ -77,7 +77,7 @@ export const defaultRecurringExpense = {
title: "",
description: "",
payee: "",
- notifyTo: "",
+ notifyTo: [],
currencyId: "",
amount: 0,
strikeDate: "",
diff --git a/src/components/RecurringExpense/ViewRecurringExpense.jsx b/src/components/RecurringExpense/ViewRecurringExpense.jsx
index 305c843c..0c7a5327 100644
--- a/src/components/RecurringExpense/ViewRecurringExpense.jsx
+++ b/src/components/RecurringExpense/ViewRecurringExpense.jsx
@@ -143,10 +143,10 @@ const ViewRecurringExpense = ({ RecurringId }) => {
{data?.notifyTo?.length > 0
- ? data.notifyTo.map((user, index) => (
+ ? data.notifyTo?.map((user, index) => (
{user.email}
- {index < data.notifyTo.length - 1 && ", "}
+ {index < data?.notifyTo?.length - 1 && ", "}
))
: "N/A"}
diff --git a/src/components/common/usesInput.jsx b/src/components/common/usesInput.jsx
index 4497c672..17e61877 100644
--- a/src/components/common/usesInput.jsx
+++ b/src/components/common/usesInput.jsx
@@ -1,87 +1,294 @@
-import { useEffect, useRef } from "react";
+import { useState, useEffect, useRef, useMemo } from "react";
import { useController } from "react-hook-form";
-import { useEmployeesName } from "../../hooks/useEmployees";
import { useDebounce } from "../../utils/appUtils";
+import { useEmployeesName } from "../../hooks/useEmployees";
import Avatar from "./Avatar";
-const UsersTagInput = ({ control, name, projectId, placeholder, forAll }) => {
- const { field } = useController({ name, control });
+const UsersTagInput = ({
+ 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 tagifyRef = useRef(null);
+ const activeIndexRef = useRef(-1);
- // debounce the search term
- const debouncedSearch = useDebounce("", 400);
- const { data: employees } = useEmployeesName(projectId, debouncedSearch, forAll);
+ const debouncedSearch = useDebounce(search, 300);
+ const { data: employees, isLoading } = useEmployeesName(
+ projectId,
+ debouncedSearch,
+ forAll
+ );
+ // Keep both filtered list and cache updated
useEffect(() => {
- if (!window.Tagify || !inputRef.current) return;
+ if (employees?.data?.length) {
+ setFilteredUsers(employees.data);
+ activeIndexRef.current = -1;
- // Initialize Tagify on the input
- const Tagify = window.Tagify;
- tagifyRef.current = new Tagify(inputRef.current, {
- enforceWhitelist: false,
- dropdown: {
- enabled: 1,
- classname: "users-list",
- searchKeys: ["value", "email"],
- },
- templates: {
- dropdownItem: (tagData) => `
-
-
- 
-
- ${tagData.value}
- ${tagData.email || ""}
-
- `,
- tag: (tagData) => `
-
-
-
-
-
- ${tagData.value}
-
-
-
- `,
- },
- });
-
- // 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);
- });
-
- return () => tagifyRef.current.destroy();
- }, []);
-
- // 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,
- }));
+ // cache all fetched users by id
+ setUserCache((prev) => {
+ const updated = { ...prev };
+ employees.data.forEach((u) => {
+ updated[u.id] = u;
+ });
+ return updated;
+ });
+ } else {
+ setFilteredUsers([]);
}
}, [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 (
-
+
+ {/* Selected tags (chips) */}
+ {value.map((id) => {
+ const u = resolveUserById(id);
+ if (!u) return null;
+ return (
+
+
+ {u.photo ? (
+
+
+
+ ) : (
+
+
+ {u.firstName?.[0] || ""}
+ {u.lastName?.[0] || ""}
+
+
+ )}
+
+
+
+ {u.firstName} {u.lastName}
+
+
+
+
+
+ );
+ })}
+
+ {
+ setSearch(e.target.value);
+ setShowDropdown(true);
+ }}
+ onFocus={() => {
+ setShowDropdown(true);
+ }}
+ onKeyDown={onInputKeyDown}
+ autoComplete="off"
+ aria-expanded={showDropdown}
+ aria-haspopup="listbox"
+ />
+
+ {showDropdown && (
+
+
+ {isLoading ? (
+
+ Loading...
+
+ ) : filteredUsers.length === 0 ? (
+
+ No users found
+
+ ) : (
+ filteredUsers.map((user, idx) => {
+ const isActive = idx === activeIndexRef.current;
+ return (
+ (activeIndexRef.current = idx)}
+ onMouseDown={(e) => {
+ e.preventDefault();
+ handleSelect(user);
+ }}
+ >
+
+ {user.photo ? (
+ 
+ ) : (
+
+ )}
+
+ {user.firstName} {user.lastName}
+
+
+
+ );
+ })
+ )}
+
+
+ )}
+
);
};
diff --git a/src/pages/AdvancePayment/AdvancePaymentPage.jsx b/src/pages/AdvancePayment/AdvancePaymentPage.jsx
index 9c080ab8..05308eb1 100644
--- a/src/pages/AdvancePayment/AdvancePaymentPage.jsx
+++ b/src/pages/AdvancePayment/AdvancePaymentPage.jsx
@@ -13,6 +13,7 @@ import Label from "../../components/common/Label";
import AdvancePaymentList from "../../components/AdvancePayment/AdvancePaymentList";
import { employee } from "../../data/masters";
import { formatFigure } from "../../utils/appUtils";
+import UsersTagInput from "../../components/common/usesInput";
export const AdvancePaymentContext = createContext();
export const useAdvancePaymentContext = () => {
@@ -26,7 +27,7 @@ export const useAdvancePaymentContext = () => {
};
const AdvancePaymentPage = () => {
const [balance, setBalance] = useState(null);
- const { control, reset, watch } = useForm({
+ const {control, reset, watch } = useForm({
defaultValues: {
employeeId: "",
},
@@ -39,6 +40,8 @@ const AdvancePaymentPage = () => {
employeeId: selectedEmpoyee || "",
});
}, [reset]);
+
+
return (
@@ -83,6 +86,8 @@ const AdvancePaymentPage = () => {
+
+
diff --git a/src/pages/RecurringExpense/RecurringExpensePage.jsx b/src/pages/RecurringExpense/RecurringExpensePage.jsx
index c20d3642..cffccaec 100644
--- a/src/pages/RecurringExpense/RecurringExpensePage.jsx
+++ b/src/pages/RecurringExpense/RecurringExpensePage.jsx
@@ -145,13 +145,13 @@ const RecurringExpensePage = () => {
setViewRecurring({ IsOpen: null, recurringId: null })
}
>
-
setViewRecurring({ IsOpen: null, recurringId: null })
}
RecurringId={viewRecurring.recurringId}
- />
+ /> */}
)}
|