diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index c03320293..1a537833a 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -7,7 +7,7 @@ import { ITimerStatusEnum, OT_Member } from '@app/interfaces'; import { clsxm, isValidUrl } from '@app/utils'; import clsx from 'clsx'; import { withAuthentication } from 'lib/app/authenticator'; -import { Avatar, Breadcrumb, Container, Text, VerticalSeparator } from 'lib/components'; +import { Avatar, Breadcrumb, Button, Container, Text, VerticalSeparator } from 'lib/components'; import { ArrowLeftIcon } from 'assets/svg'; import { TaskFilter, Timer, TimerStatus, UserProfileTask, getTimerStatusValue, useTaskFilter } from 'lib/features'; import { MainHeader, MainLayout } from 'lib/layout'; @@ -29,6 +29,7 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId const profile = useUserProfilePage(); const { user } = useAuthenticateUser(); const { isTrackingEnabled, activeTeam, activeTeamManagers } = useOrganizationTeams(); + const members = activeTeam?.members; const { getEmployeeDayPlans } = useDailyPlan(); const fullWidth = useRecoilValue(fullWidthState); const [activityFilter, setActivityFilter] = useState('Tasks'); @@ -74,72 +75,93 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId getEmployeeDayPlans(profile.member?.employeeId ?? ''); }, [getEmployeeDayPlans, profile.member?.employeeId]); - // Example usage - return ( <> - - - {/* Breadcrumb */} -
- - - - - + {Array.isArray(members) && members.length && !profile.member ? ( + +
+
+ + {t('common.MEMBER')} {t('common.NOT_FOUND')}! + + + + {t('pages.profile.MEMBER_NOT_FOUND_MSG_1')} + + + {t('pages.profile.MEMBER_NOT_FOUND_MSG_1')} + + + +
+
+ ) : ( + + + {/* Breadcrumb */} +
+ + + - {/* User Profile Detail */} -
- - - {profileIsAuthUser && isTrackingEnabled && ( - - )} -
- {/* TaskFilter */} - - - {/* Divider */} -
- {hook.tab == 'worked' && canSeeActivity && ( - -
- {Object.keys(activityScreens).map((filter, i) => ( -
- {i !== 0 && } -
changeActivityFilter(filter as FilterTab)} - > - {filter} -
-
- ))} +
-
- )} - - {hook.tab !== 'worked' || activityFilter == 'Tasks' ? ( - - ) : ( - activityScreens[activityFilter] ?? null + {/* User Profile Detail */} +
+ + + {profileIsAuthUser && isTrackingEnabled && ( + + )} +
+ {/* TaskFilter */} + + + {/* Divider */} +
+ {hook.tab == 'worked' && canSeeActivity && ( + +
+ {Object.keys(activityScreens).map((filter, i) => ( +
+ {i !== 0 && } +
changeActivityFilter(filter as FilterTab)} + > + {filter} +
+
+ ))} +
+
)} -
- + + + {hook.tab !== 'worked' || activityFilter == 'Tasks' ? ( + + ) : ( + activityScreens[activityFilter] ?? null + )} + + + )} ); }); diff --git a/apps/web/app/hooks/features/useLanguageSettings.ts b/apps/web/app/hooks/features/useLanguageSettings.ts index 16bad43a6..f5bd7c535 100644 --- a/apps/web/app/hooks/features/useLanguageSettings.ts +++ b/apps/web/app/hooks/features/useLanguageSettings.ts @@ -39,14 +39,10 @@ export function useLanguageSettings() { const loadLanguagesData = useCallback(() => { setActiveLanguageId(getActiveLanguageIdCookie()); - if (user) { - return queryCall(user.role.isSystem).then((res) => { - setLanguages( - res?.data?.items.filter((item: any) => APPLICATION_LANGUAGES_CODE.includes(item.code)) || [] - ); - return res; - }); - } + return queryCall(user?.role?.isSystem ?? false).then((res) => { + setLanguages(res?.data?.items.filter((item: any) => APPLICATION_LANGUAGES_CODE.includes(item.code)) || []); + return res; + }); }, [queryCall, setActiveLanguageId, setLanguages, user]); const setActiveLanguage = useCallback( diff --git a/apps/web/lib/components/image-overlapper.tsx b/apps/web/lib/components/image-overlapper.tsx index c7e2f1a38..a270db41d 100644 --- a/apps/web/lib/components/image-overlapper.tsx +++ b/apps/web/lib/components/image-overlapper.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; import Image from 'next/image'; import Link from 'next/link'; @@ -12,8 +13,10 @@ import { useOrganizationTeams } from '@app/hooks'; import { useTranslations } from 'next-intl'; import { TaskAssignButton } from '../../lib/features/task/task-assign-button'; import { clsxm } from '@app/utils'; - +import { TaskAvatars } from 'lib/features'; +import { FaCheck } from "react-icons/fa6"; import TeamMember from 'lib/components/team-member'; +import { IEmployee } from '@app/interfaces'; export interface ImageOverlapperProps { id: string; @@ -22,14 +25,13 @@ export interface ImageOverlapperProps { } interface ArrowDataProps { - activeTaskStatus: ITimerStatus | null | undefined; + activeTaskStatus: ITimerStatus | null | undefined; disabled: boolean; task: ITeamTask; - className: string | undefined; - iconClassName: string | undefined; + className: string | undefined; + iconClassName: string | undefined; } - export default function ImageOverlapper({ images, radius = 20, @@ -38,7 +40,8 @@ export default function ImageOverlapper({ diameter = 40, iconType = false, arrowData = null, - hasActiveMembers = false + hasActiveMembers = false, + assignTaskButtonCall = false, }: { images: ImageOverlapperProps[]; radius?: number; @@ -48,6 +51,7 @@ export default function ImageOverlapper({ iconType?: boolean; arrowData?: ArrowDataProps | null; hasActiveMembers?: boolean; + assignTaskButtonCall?: boolean; }) { // Split the array into two arrays based on the display number const firstArray = images.slice(0, displayImageCount); @@ -58,15 +62,39 @@ export default function ImageOverlapper({ const { isOpen, openModal, closeModal } = useModal(); const { activeTeam } = useOrganizationTeams(); const allMembers = activeTeam?.members || []; + const [assignedMembers, setAssignedMembers] = useState([...(item?.members || [])]); + const [unassignedMembers, setUnassignedMembers] = useState([]); + const [validate, setValidate] = useState(false); const t = useTranslations(); - const hasMembers = item?.members.length > 0; + const onCheckMember = (member: any) => { + const checkUser = assignedMembers.some((el: IEmployee) => el.id === member.id); + if (checkUser) { + const updatedMembers = assignedMembers.filter((el: IEmployee) => el.id != member.id); + setAssignedMembers(updatedMembers); + setUnassignedMembers([...unassignedMembers, member]); + } else { + setAssignedMembers([...assignedMembers, member]); + const updatedUnassign = unassignedMembers.filter((el: IEmployee) => el.id != member.id); + setUnassignedMembers(updatedUnassign); + } + + } + + const onCLickValidate = () => { + setValidate(!validate); + closeModal(); + } + + const hasMembers = item?.members?.length > 0; + const membersList = { assignedMembers, unassignedMembers }; if (imageLength == undefined) { return ; } - if ((!hasMembers && item) || hasActiveMembers) { + + if ((!hasMembers && item) || hasActiveMembers || assignTaskButtonCall) { return (
{ @@ -88,22 +116,48 @@ export default function ImageOverlapper({ isOpen={isOpen} closeModal={closeModal} title={t('common.SELECT_TEAM_MEMBER')} - className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:w-[20vw] h-[45vh] justify-start" + className="bg-light--theme-light dark:bg-dark--theme-light py-5 rounded-xl w-full md:min-w-[20vw] md:max-w-fit h-[45vh] justify-start" titleClass="font-normal" > -
    +
      {allMembers?.map((member: any) => { return (
    • - -
    • + key={member.employee} + className="w-100 border border-transparent hover:border-blue-500 hover:border-opacity-50 rounded-lg cursor-pointer" + > + + ); })}
    + +
    + +
    + +
    + +
    +
    +
diff --git a/apps/web/lib/components/team-member.tsx b/apps/web/lib/components/team-member.tsx index 7014d07f9..dd59d488a 100644 --- a/apps/web/lib/components/team-member.tsx +++ b/apps/web/lib/components/team-member.tsx @@ -1,22 +1,40 @@ import { UserInfo } from 'lib/features/team/user-team-card/user-info'; import { useTeamMemberCard } from '@app/hooks'; -import { useModal } from '@app/hooks'; +import { FaCheck } from "react-icons/fa6"; +import { useEffect } from 'react'; +import { IEmployee } from '@app/interfaces'; -export default function TeamMember({ member, item }: { member: any; item: any }) { +export default function TeamMember( + { member, item, onCheckMember, membersList, validate } + : + { member: any; item: any; onCheckMember: any; membersList: any; validate: any } +) { const memberInfo = useTeamMemberCard(member); const { assignTask } = useTeamMemberCard(member); - const { closeModal } = useModal(); + const checkAssign = membersList.assignedMembers.some((el:IEmployee) => el.id === member.employeeId); + const checkUnassign = membersList.unassignedMembers.some((el:IEmployee) => el.id === member.employeeId); - return ( -
{ + useEffect(() => { + if (validate) { + if (checkAssign) { assignTask(item); - closeModal(); - }} + } else if (checkUnassign) { + memberInfo.unassignTask(item); + } + } + }, [validate, checkAssign, checkUnassign, item, assignTask, memberInfo]); + + const assignMember = () => { + onCheckMember(member.employee); + } - className="w-100 cursor-pointer" + return ( +
{ assignMember() }} + className="w-100 cursor-pointer flex items-center" > + {checkAssign ? () : (<>)}
); } diff --git a/apps/web/lib/features/task/daily-plan/outstanding.tsx b/apps/web/lib/features/task/daily-plan/outstanding.tsx index 9a3794c00..e5a0e65ec 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding.tsx @@ -21,16 +21,17 @@ export function Outstanding({ dayPlans, profile }: { dayPlans: IDailyPlan[]; pro ...plan, // Include only no completed tasks tasks: plan.tasks?.filter((task) => task.status !== 'completed') - })); + })) + .filter((plan) => plan.tasks?.length && plan.tasks.length > 0); return (
- {filteredPlans.length > 0 ? ( + {filteredPlans?.length > 0 ? ( new Date(plan.date).toISOString().split('T')[0])} + defaultValue={filteredPlans?.map((plan) => new Date(plan.date).toISOString().split('T')[0])} > - {filteredPlans.map((plan) => ( + {filteredPlans?.map((plan) => ( - {filteredPlans.length > 0 ? ( + {filteredPlans?.length > 0 ? ( - {filteredPlans.map((plan) => ( + {filteredPlans?.map((plan) => ( 0; - const taskAssignee: ImageOverlapperProps[] = task?.members.map((member: any) => { - return { - id: member.user.id, - url: member.user.imageUrl, - alt: member.user.firstName - }; - }) || []; - + const activeMembers = task != null && task?.members?.length > 0; + const taskAssignee: ImageOverlapperProps[] = + task?.members?.map((member: any) => { + return { + id: member.user.id, + url: member.user.imageUrl, + alt: member.user.firstName + }; + }) || []; return ( <> @@ -170,7 +170,12 @@ export function TaskCard(props: Props) { {viewType === 'unassign' && (
- +
)} @@ -197,7 +202,7 @@ export function TaskCard(props: Props) { )} @@ -388,7 +393,7 @@ function AssignTaskButtonCall({ task, className, iconClassName, - taskAssignee, + taskAssignee }: { task: ITeamTask; className?: string; @@ -405,12 +410,14 @@ function AssignTaskButtonCall({ const activeTaskStatus = activeTeamTask?.id === task.id ? timerStatus : undefined; const arrowData = { - activeTaskStatus, disabled, task, className, iconClassName - } + activeTaskStatus, + disabled, + task, + className, + iconClassName + }; - return ( - - ); + return ; } //* Task Estimate info * diff --git a/apps/web/lib/features/task/task-item.tsx b/apps/web/lib/features/task/task-item.tsx index 27c2b8759..f84d3323f 100644 --- a/apps/web/lib/features/task/task-item.tsx +++ b/apps/web/lib/features/task/task-item.tsx @@ -1,6 +1,6 @@ import { imgTitle } from '@app/helpers'; import { useTeamTasks } from '@app/hooks'; -import { IClassName, ITaskStatus, ITeamTask } from '@app/interfaces'; +import { IClassName, ITaskStatus, IEmployee, ITeamTask } from '@app/interfaces'; import { clsxm, isValidUrl } from '@app/utils'; import clsx from 'clsx'; import { Avatar, ConfirmDropdown, SpinnerLoader, Tooltip } from 'lib/components'; @@ -118,9 +118,10 @@ export function TaskItem({ task, selected, onClick, className }: Props) { ); } -export function TaskAvatars({ task, limit = 2 }: { task: ITeamTask; limit?: number }) { - const members = task.members; +type PartialITeamTask = Partial & { members: IEmployee[] }; +export function TaskAvatars({ task, limit = 2 }: { task: PartialITeamTask; limit?: number }) { + const members = task.members; const taskAssignee: ImageOverlapperProps[] = members.map((member: any) => { return { id: member.user.id, diff --git a/apps/web/lib/features/team/user-team-card/index.tsx b/apps/web/lib/features/team/user-team-card/index.tsx index 041bfee1b..0f7de2723 100644 --- a/apps/web/lib/features/team/user-team-card/index.tsx +++ b/apps/web/lib/features/team/user-team-card/index.tsx @@ -25,6 +25,8 @@ import UserTeamActivity from './user-team-card-activity'; import { CollapseUpIcon, ExpandIcon } from '@components/ui/svgs/expand'; import { activityTypeState } from '@app/stores/activity-type'; import { SixSquareGridIcon } from 'assets/svg'; +import { ChevronDoubleDownIcon } from '@heroicons/react/20/solid'; +import { useRouter } from 'next/navigation'; type IUserTeamCard = { active?: boolean; @@ -53,13 +55,14 @@ export function UserTeamCard({ const t = useTranslations(); const memberInfo = useTeamMemberCard(member); const taskEdition = useTMCardTaskEdit(memberInfo.memberTask); - + const { replace } = useRouter(); const { collaborativeSelect, user_selected, onUserSelect } = useCollaborative(memberInfo.memberUser); const seconds = useRecoilValue(timerSecondsState); const setActivityFilter = useSetRecoilState(activityTypeState); const { activeTaskTotalStat, addSeconds } = useTaskStatistics(seconds); const [showActivity, setShowActivity] = React.useState(false); + const [userDetailAccordion, setUserDetailAccordion] = React.useState(false); const { activeTeamManagers } = useOrganizationTeams(); const { user } = useAuthenticateUser(); @@ -124,7 +127,7 @@ export function UserTeamCard({
+ {userDetailAccordion ?
: null} - {filteredPlans.length > 0 ? ( + {filteredPlans?.length > 0 ? ( new Date(plan.date).toISOString().split('T')[0])[0]] + : [filteredPlans?.map((plan) => new Date(plan.date).toISOString().split('T')[0])[0]] } > - {filteredPlans.map((plan) => ( + {filteredPlans?.map((plan) => (