@@ -53,9 +52,10 @@ const UserOutstandingNotification = memo(function UserOutstandingNotification({
const [visible, setVisible] = useState(false);
- const outStandingTasksCount = estimatedTotalTime(
- outstandingPlans.map((plan) => plan.tasks?.map((task) => task))
- ).totalTasks;
+ const outStandingTasksCount = useMemo(
+ () => estimatedTotalTime(outstandingPlans.map((plan) => plan.tasks?.map((task) => task))).totalTasks,
+ [outstandingPlans]
+ );
const lastVisited = window?.localStorage.getItem(HAS_VISITED_OUTSTANDING_TASKS);
@@ -131,22 +131,23 @@ const ManagerOutstandingUsersNotification = memo(function ManagerOutstandingUser
const [visible, setVisible] = useState(false);
- const employeeWithOutstanding = outstandingTasks
- .filter((plan) => plan.employeeId !== user?.employee.id)
- .filter((plan) => !plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0]))
-
- .filter((plan) => {
- const planDate = new Date(plan.date);
- const today = new Date();
- today.setHours(23, 59, 59, 0);
- return planDate.getTime() <= today.getTime();
- })
- .map((plan) => ({
- ...plan,
- tasks: plan.tasks?.filter((task) => task.status !== 'completed')
- }))
- .filter((plan) => plan.tasks?.length && plan.tasks.length > 0)
- .map((plan) => ({ employeeId: plan.employeeId, employee: plan.employee }));
+ const employeeWithOutstanding = useMemo(
+ () =>
+ outstandingTasks
+
+ .filter((plan) => {
+ if (plan.employeeId === user?.employee.id) return false;
+ if (!plan.date) return false;
+
+ const isTodayOrBefore = moment(plan.date).isSameOrBefore(moment().endOf('day'));
+ if (!isTodayOrBefore) return false;
+
+ const hasIncompleteTasks = plan.tasks?.some((task) => task.status !== 'completed');
+ return hasIncompleteTasks;
+ })
+ .map((plan) => ({ employeeId: plan.employeeId, employee: plan.employee })),
+ [outstandingTasks, user?.employee.id]
+ );
const uniqueEmployees: IEmployeeWithOutstanding[] = employeeWithOutstanding.reduce(
(acc: IEmployeeWithOutstanding[], current) => {
diff --git a/apps/web/lib/settings/edit-role-dropdown.tsx b/apps/web/lib/settings/edit-role-dropdown.tsx
new file mode 100644
index 000000000..80bf48988
--- /dev/null
+++ b/apps/web/lib/settings/edit-role-dropdown.tsx
@@ -0,0 +1,45 @@
+import { Dropdown } from 'lib/components';
+import { clsxm } from '@app/utils';
+import {useRoles} from "@app/hooks/features/useRoles";
+import { IRole, IRoleList, OT_Member } from "@app/interfaces";
+import {useCallback, useEffect, useMemo, useState} from "react";
+import {mapRoleItems, RoleItem} from "../features/roles/role-item";
+
+
+export const EditUserRoleDropdown = ({ member, handleRoleChange }: { member: OT_Member,handleRoleChange : (newRole: IRole) => void}) => {
+ const {roles} = useRoles()
+
+ const items = useMemo(() => mapRoleItems(roles.filter(role => ['MANAGER', 'EMPLOYEE'].includes(role.name)) as IRoleList[]), [roles]);
+
+ const [roleItem, setRoleItem] = useState
(null);
+
+ useEffect(() => {
+ setRoleItem(items.find(t => t.key === member?.roleId) || null);
+ }, [items, member?.roleId]);
+
+ const onChange = useCallback(
+ (item: RoleItem) => {
+ if (item.data) {
+ setRoleItem(item);
+ }
+ if (item.data?.id && item.data.data) {
+ handleRoleChange(item.data?.data)
+ }
+ },
+ [handleRoleChange]
+ );
+
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/apps/web/lib/settings/member-table.tsx b/apps/web/lib/settings/member-table.tsx
index 7d123fefd..83caa5430 100644
--- a/apps/web/lib/settings/member-table.tsx
+++ b/apps/web/lib/settings/member-table.tsx
@@ -1,268 +1,312 @@
import { CHARACTER_LIMIT_TO_SHOW } from '@app/constants';
import { imgTitle } from '@app/helpers';
-import { useSettings } from '@app/hooks';
+import { useOrganizationTeams, useSettings, useSyncRef } from '@app/hooks';
import { usePagination } from '@app/hooks/features/usePagination';
-import { OT_Member, OT_Role } from '@app/interfaces';
+import { IRole, OT_Member, OT_Role } from '@app/interfaces';
import { activeTeamIdState, organizationTeamsState } from '@app/stores';
import { clsxm } from '@app/utils';
import { Avatar, InputField, Text, Tooltip } from 'lib/components';
import { Paginate } from 'lib/components/pagination';
import cloneDeep from 'lodash/cloneDeep';
import moment from 'moment';
-import { ChangeEvent, KeyboardEvent, useCallback, useState } from 'react';
+import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react';
import { useTranslations } from 'next-intl';
import { useAtom, useAtomValue } from 'jotai';
import stc from 'string-to-color';
import { MemberTableStatus } from './member-table-status';
import { TableActionPopover } from './table-action-popover';
+import { EditUserRoleDropdown } from './edit-role-dropdown';
export const MemberTable = ({ members }: { members: OT_Member[] }) => {
- const t = useTranslations();
- const {
- total,
- onPageChange,
- itemsPerPage,
- itemOffset,
- endOffset,
- setItemsPerPage,
- currentItems
- } = usePagination(members);
- const { updateAvatar } = useSettings();
+ const t = useTranslations();
+ const { total, onPageChange, itemsPerPage, itemOffset, endOffset, setItemsPerPage, currentItems } =
+ usePagination(members);
+ const { activeTeam, updateOrganizationTeam,} = useOrganizationTeams();
+ const { updateAvatar } = useSettings();
- const activeTeamId = useAtomValue(activeTeamIdState);
- const [organizationTeams, setOrganizationTeams] = useAtom(
- organizationTeamsState
- );
- const [editMember, setEditMember] = useState(null);
- const handleEdit = (member: OT_Member) => {
- setEditMember(member);
- };
- const handelNameChange = useCallback(
- (event: ChangeEvent) => {
- const name = event.target.value || '';
- if (name === editMember?.employee.fullName) {
- return;
- }
+ const activeTeamRef = useSyncRef(activeTeam);
- const names = name.split(' ');
- const tempMember: OT_Member | null = cloneDeep(editMember);
- if (tempMember && tempMember.employee.user) {
- tempMember.employee.fullName = name;
- tempMember.employee.user.firstName = names[0] || '';
- tempMember.employee.user.lastName = names[1] || '';
- setEditMember(tempMember);
- }
- },
- [editMember]
- );
- const handleEditMemberSave = useCallback(() => {
- if (editMember) {
- updateAvatar({
- firstName: editMember?.employee?.user?.firstName || '',
- lastName: editMember?.employee?.user?.lastName || '',
- id: editMember?.employee?.userId
- }).then(() => {
- const teamIndex = organizationTeams.findIndex(
- (team) => team.id === activeTeamId
- );
- const tempOrganizationTeams = cloneDeep(organizationTeams);
- const memberIndex = tempOrganizationTeams[teamIndex].members.findIndex(
- (member) => member.id === editMember.id
- );
+ const activeTeamId = useAtomValue(activeTeamIdState);
+ const [organizationTeams, setOrganizationTeams] = useAtom(organizationTeamsState);
+ const editMemberRef = useRef(null);
- tempOrganizationTeams[teamIndex].members[memberIndex] = editMember;
- setOrganizationTeams(tempOrganizationTeams);
- setEditMember(null);
- });
- }
- }, [
- editMember,
- organizationTeams,
- activeTeamId,
- setOrganizationTeams,
- updateAvatar
- ]);
- const handleOnKeyUp = (event: KeyboardEvent) => {
- if (event.key === 'Enter') {
- handleEditMemberSave();
- }
- };
+ const updateTeamMember = useCallback((updatedMember: OT_Member) => {
+ const teamIndex = organizationTeams.findIndex((team) => team.id === activeTeamId);
+ if (teamIndex === -1) return;
- return (
-
-
-
-
-
-
- {t('common.NAME')}
- |
-
- {t('common.POSITION')}
- |
-
- {t('common.ROLES')}
- |
-
- {t('common.JOIN_OR_LEFT')}
- |
-
- {t('common.STATUS')}
- |
- |
-
-
-
- {currentItems.map((member, index) => (
-
-
- {member.employee.user?.imageId ? (
-
- ) : member.employee.user?.name ? (
-
- {imgTitle(member.employee.user?.name)}
-
- ) : (
- ''
- )}
-
- {editMember && editMember.id === member.id ? (
-
- ) : (
-
- CHARACTER_LIMIT_TO_SHOW
- }
- >
- {
- handleEdit(member);
- }}
- >
- {member.employee.fullName}
-
-
- )}
+ const tempTeams = cloneDeep(organizationTeams);
+ const memberIndex = tempTeams[teamIndex].members.findIndex(
+ (member) => member.id === updatedMember.id);
-
- CHARACTER_LIMIT_TO_SHOW
- }
- >
-
- {member.employee.user?.email || ''}
-
-
-
- |
-
- {/* TODO Position */}-
- |
-
-
- {getRoleString(member.role)}
-
- |
-
- {/* 12 Feb 2020 12:00 pm */}
- {moment(member.employee.createdAt).format(
- 'DD MMM YYYY hh:mm a'
- )}
- |
-
- {/* TODO dynamic */}
-
- |
-
-
- |
-
- ))}
-
-
-
+ if (memberIndex === -1) return;
-
-
- );
+ tempTeams[teamIndex].members[memberIndex] = updatedMember;
+ setOrganizationTeams(tempTeams);
+ }, [activeTeamId, organizationTeams, setOrganizationTeams]);
+
+ const handleEdit = useCallback((member: OT_Member) => {
+ editMemberRef.current = member;
+ }, []);
+
+
+ const handleManagerRoleUpdate = useCallback((employeeId: string, isPromotingToManager: boolean) => {
+ if (!activeTeamRef.current) return;
+
+ // Get current managers
+ const currentManagers: string[] = activeTeamRef.current?.members
+ .filter((member: OT_Member) => member.role?.name === 'MANAGER')
+ .map((manager: OT_Member) => manager.employee.id) || [];
+
+ if (isPromotingToManager) {
+ // Add new manager
+ const updatedMemberIds = [...new Set([
+ ...(activeTeamRef.current?.members || []).map((member: OT_Member) => member.employee.id),
+ employeeId
+ ])];
+
+ return updateOrganizationTeam(activeTeamRef.current, {
+ ...activeTeamRef.current,
+ memberIds: updatedMemberIds
+ });
+ } else {
+ // Remove manager
+ const updatedMemberIds = [...new Set([...currentManagers, employeeId])];
+ const updatedManagerIds = currentManagers.filter(id => id !== employeeId);
+
+ return updateOrganizationTeam(activeTeamRef.current, {
+ ...activeTeamRef.current,
+ memberIds: updatedMemberIds,
+ managerIds: updatedManagerIds,
+ });
+ }
+
+ }, [updateOrganizationTeam, activeTeamRef]);
+
+
+ const handleRoleChange = useCallback((newRole: IRole) => {
+ if (!editMemberRef.current || !activeTeamRef.current) return;
+
+ console.log({ newRole })
+
+ const { employeeId, role } = editMemberRef.current;
+
+ const isPromotingToManager = role?.name !== 'MANAGER' && newRole?.name === 'MANAGER';
+ handleManagerRoleUpdate(employeeId, isPromotingToManager);
+
+ // Update Organization Team
+ // const updatedMember = { ...editMemberRef.current, roleId: !isPromotingToManager ? '' : };
+ // updateTeamMember(updatedMember);
+ editMemberRef.current = null;
+
+ }, [activeTeamRef, handleManagerRoleUpdate]);
+
+ const handelNameChange = useCallback(
+ (event: ChangeEvent) => {
+ const name = event.target.value || '';
+ if (name === editMemberRef.current?.employee.fullName) {
+ return;
+ }
+
+ const names = name.split(' ');
+ const tempMember: OT_Member | null = cloneDeep(editMemberRef.current);
+
+ if (tempMember?.employee?.user) {
+ tempMember.employee.fullName = name;
+ tempMember.employee.user.firstName = names[0] || '';
+ tempMember.employee.user.lastName = names[1] || '';
+ editMemberRef.current = tempMember;
+ }
+ },
+ []
+ );
+ const handleEditMemberSave = useCallback(() => {
+ const member = editMemberRef.current;
+ if (member) {
+ updateAvatar({
+ firstName: member.employee?.user?.firstName || '',
+ lastName: member.employee?.user?.lastName || '',
+ id: member.employee?.userId || ''
+ }).then(() => {
+ if (member) {
+ updateTeamMember(member);
+ }
+ });
+ }
+ }, [updateAvatar, updateTeamMember]);
+
+ const handleOnKeyUp = (event: KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ handleEditMemberSave();
+ }
+ editMemberRef.current = null;
+ };
+
+ return (
+
+
+
+
+
+
+ {t('common.NAME')}
+ |
+
+ {t('common.POSITION')}
+ |
+
+ {t('common.ROLES')}
+ |
+
+ {t('common.JOIN_OR_LEFT')}
+ |
+
+ {t('common.STATUS')}
+ |
+ |
+
+
+
+ {currentItems.map((member, index) => (
+
+
+ {member.employee.user?.imageId ? (
+
+ ) : member.employee.user?.name ? (
+
+ {imgTitle(member.employee.user?.name)}
+
+ ) : (
+ ''
+ )}
+
+ {editMemberRef.current && editMemberRef.current.id === member.id ? (
+
+ ) : (
+ CHARACTER_LIMIT_TO_SHOW
+ }
+ >
+ {
+ handleEdit(member);
+ }}
+ >
+ {member.employee.fullName}
+
+
+ )}
+
+
+ CHARACTER_LIMIT_TO_SHOW
+ }
+ >
+
+ {member.employee.user?.email || ''}
+
+
+
+ |
+
+ {/* TODO Position */}-
+ |
+ handleEdit(member)}
+ >
+ {editMemberRef.current && editMemberRef.current.id === member.id ? (
+
+ ) : (
+ {getRoleString(member.role)}
+ )}
+ |
+
+ {/* 12 Feb 2020 12:00 pm */}
+ {moment(member.employee.createdAt).format('DD MMM YYYY hh:mm a')}
+ |
+
+ {/* TODO dynamic */}
+
+ |
+
+
+ |
+
+ ))}
+
+
+
+
+
+
+ );
};
const getRoleString = (role: OT_Role | undefined) => {
- return role?.name || 'MEMBER';
+ return role?.name || 'MEMBER';
};
diff --git a/apps/web/lib/settings/table-action-popover.tsx b/apps/web/lib/settings/table-action-popover.tsx
index 3cdab0e82..c74d1ed9d 100644
--- a/apps/web/lib/settings/table-action-popover.tsx
+++ b/apps/web/lib/settings/table-action-popover.tsx
@@ -105,7 +105,8 @@ export const TableActionPopover = ({ member, handleEdit, status }: Props) => {
{isLoading ?
: null
- }
+ }
+
}
diff --git a/yarn.lock b/yarn.lock
index e8f555736..564b95bee 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13476,9 +13476,9 @@ electronmon@^2.0.2:
watchboy "^0.4.3"
elliptic@^6.5.3, elliptic@^6.5.4:
- version "6.5.7"
- resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b"
- integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.0.tgz#5919ec723286c1edf28685aa89261d4761afa210"
+ integrity sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"
@@ -24712,7 +24712,16 @@ string-to-color@^2.2.2:
lodash.words "^4.2.0"
rgb-hex "^3.0.0"
-"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -24822,7 +24831,14 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -26872,7 +26888,7 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -26890,6 +26906,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"