From 0337cceeb822affa00dbd9b6850c45d66a1002ad Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Sat, 29 Jun 2024 20:05:25 +0200 Subject: [PATCH 01/10] feat: manager notification for outstanding users tasks --- .../team/team-outstanding-notifications.tsx | 82 ++++++++++++++++++- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/apps/web/lib/features/team/team-outstanding-notifications.tsx b/apps/web/lib/features/team/team-outstanding-notifications.tsx index afd691602..7b792cc94 100644 --- a/apps/web/lib/features/team/team-outstanding-notifications.tsx +++ b/apps/web/lib/features/team/team-outstanding-notifications.tsx @@ -8,20 +8,27 @@ import Link from 'next/link'; import { useEffect, useState } from 'react'; export function TeamOutstandingNotifications() { - const { getEmployeeDayPlans, outstandingPlans } = useDailyPlan(); + const { getAllDayPlans, dailyPlan, getEmployeeDayPlans, outstandingPlans } = useDailyPlan(); const { user } = useAuthenticateUser(); useEffect(() => { + getAllDayPlans(); getEmployeeDayPlans(user?.employee.id || ''); - }, [getEmployeeDayPlans, user?.employee.id]); + }, [getAllDayPlans, getEmployeeDayPlans, user?.employee.id]); + + console.log(dailyPlan); return ( - <> +
{outstandingPlans && outstandingPlans.length > 0 && ( )} - + + {dailyPlan.items && dailyPlan.items.length > 0 && ( + + )} +
); } @@ -93,3 +100,70 @@ function UserOutstandingNotification({ outstandingTasks, user }: { outstandingTa ); } + +function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstandingTasks: IDailyPlan[] }) { + const t = useTranslations(); + + // Notification will be displayed 6 hours after the user closed it + const REAPPEAR_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours in milliseconds; + const DISMISSAL_TIMESTAMP_KEY = 'manager-saw-outstanding-notif'; + + const [visible, setVisible] = useState(false); + + const employeeWithOutstanding = outstandingTasks.map((plan) => plan.employee); + + useEffect(() => { + const checkNotification = () => { + const alreadySeen = window && parseInt(window?.localStorage.getItem(DISMISSAL_TIMESTAMP_KEY) || '0', 10); + const currentTime = new Date().getTime(); + + if (!alreadySeen || currentTime - alreadySeen > REAPPEAR_INTERVAL) { + setVisible(true); + } + }; + + checkNotification(); + const intervalId = setInterval(checkNotification, REAPPEAR_INTERVAL); + return () => clearInterval(intervalId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onClose = () => { + window && window?.localStorage.setItem(DISMISSAL_TIMESTAMP_KEY, new Date().getTime().toString()); + setVisible(false); + }; + return ( + <> + {visible && ( +
+
+ {t('pages.home.OUTSTANDING_NOTIFICATIONS.SUBJECT')} {employeeWithOutstanding?.length}{' '} + {t('pages.home.OUTSTANDING_NOTIFICATIONS.USER_LABEL')}{' '} + + {t('pages.home.OUTSTANDING_NOTIFICATIONS.OUTSTANDING_VIEW')} + +
+
+
+ {/* { + onClose(); + window && window.localStorage.setItem('task-tab', 'dailyplan'); + window && window.localStorage.setItem('daily-plan-tab', 'Outstanding'); + }} + > + + {t('pages.home.OUTSTANDING_NOTIFICATIONS.VIEW_BUTTON')} + */} +
+ + + +
+
+ )} + + ); +} From 0c27dd785944671509d251837f8ce39531361290 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 9 Jul 2024 18:43:55 +0200 Subject: [PATCH 02/10] feat: outstanding users notification for manager --- .../web/app/services/client/api/daily-plan.ts | 2 +- .../team/team-outstanding-notifications.tsx | 82 +++++++++++++------ 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/apps/web/app/services/client/api/daily-plan.ts b/apps/web/app/services/client/api/daily-plan.ts index e405a3b80..a99f1596e 100644 --- a/apps/web/app/services/client/api/daily-plan.ts +++ b/apps/web/app/services/client/api/daily-plan.ts @@ -14,7 +14,7 @@ export function getAllDayPlansAPI() { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - const relations = ['employee', 'tasks']; + const relations = ['employee', 'tasks', 'employee.user']; const obj = { 'where[organizationId]': organizationId, diff --git a/apps/web/lib/features/team/team-outstanding-notifications.tsx b/apps/web/lib/features/team/team-outstanding-notifications.tsx index 7b792cc94..c172d1ccb 100644 --- a/apps/web/lib/features/team/team-outstanding-notifications.tsx +++ b/apps/web/lib/features/team/team-outstanding-notifications.tsx @@ -1,16 +1,21 @@ 'use client'; import { useAuthenticateUser, useDailyPlan } from '@app/hooks'; -import { IDailyPlan, IUser } from '@app/interfaces'; +import { IDailyPlan, IEmployee, IUser } from '@app/interfaces'; import { Cross2Icon, EyeOpenIcon } from '@radix-ui/react-icons'; import { Tooltip } from 'lib/components'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { useEffect, useState } from 'react'; +interface IEmployeeWithOutstanding { + employeeId: string | undefined; + employee: IEmployee | undefined; +} + export function TeamOutstandingNotifications() { const { getAllDayPlans, dailyPlan, getEmployeeDayPlans, outstandingPlans } = useDailyPlan(); - const { user } = useAuthenticateUser(); + const { isTeamManager, user } = useAuthenticateUser(); useEffect(() => { getAllDayPlans(); @@ -25,7 +30,7 @@ export function TeamOutstandingNotifications() { )} - {dailyPlan.items && dailyPlan.items.length > 0 && ( + {dailyPlan.items && dailyPlan.items.length > 0 && isTeamManager && ( )} @@ -106,15 +111,46 @@ function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstanding // Notification will be displayed 6 hours after the user closed it const REAPPEAR_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours in milliseconds; - const DISMISSAL_TIMESTAMP_KEY = 'manager-saw-outstanding-notif'; + const MANAGER_DISMISSAL_TIMESTAMP_KEY = 'manager-saw-outstanding-notif'; const [visible, setVisible] = useState(false); - const employeeWithOutstanding = outstandingTasks.map((plan) => plan.employee); + const employeeWithOutstanding = outstandingTasks + .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 uniqueEmployees: IEmployeeWithOutstanding[] = employeeWithOutstanding.reduce( + (acc: IEmployeeWithOutstanding[], current) => { + const existingEmployee = acc.find((emp) => emp.employeeId === current.employeeId); + + if (!existingEmployee) { + acc.push({ + employeeId: current.employeeId, + employee: current.employee + }); + } + + return acc; + }, + [] + ); useEffect(() => { const checkNotification = () => { - const alreadySeen = window && parseInt(window?.localStorage.getItem(DISMISSAL_TIMESTAMP_KEY) || '0', 10); + const alreadySeen = + window && parseInt(window?.localStorage.getItem(MANAGER_DISMISSAL_TIMESTAMP_KEY) || '0', 10); const currentTime = new Date().getTime(); if (!alreadySeen || currentTime - alreadySeen > REAPPEAR_INTERVAL) { @@ -129,35 +165,29 @@ function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstanding }, []); const onClose = () => { - window && window?.localStorage.setItem(DISMISSAL_TIMESTAMP_KEY, new Date().getTime().toString()); + window && window?.localStorage.setItem(MANAGER_DISMISSAL_TIMESTAMP_KEY, new Date().getTime().toString()); setVisible(false); }; return ( <> {visible && ( -
+
- {t('pages.home.OUTSTANDING_NOTIFICATIONS.SUBJECT')} {employeeWithOutstanding?.length}{' '} - {t('pages.home.OUTSTANDING_NOTIFICATIONS.USER_LABEL')}{' '} - - {t('pages.home.OUTSTANDING_NOTIFICATIONS.OUTSTANDING_VIEW')} + {t('pages.home.OUTSTANDING_NOTIFICATIONS.SUBJECT')} {uniqueEmployees?.length} team member(s) + with unconpleted tasks, please see{' '} + + {uniqueEmployees?.map((em) => ( + + {em.employee?.fullName},{' '} + + ))}
-
- {/* { - onClose(); - window && window.localStorage.setItem('task-tab', 'dailyplan'); - window && window.localStorage.setItem('daily-plan-tab', 'Outstanding'); - }} - > - - {t('pages.home.OUTSTANDING_NOTIFICATIONS.VIEW_BUTTON')} - */} -
From e334946b77403d5b21b90a3b9384d2deed99abf7 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 9 Jul 2024 21:40:27 +0200 Subject: [PATCH 03/10] feat: collapse the invitations accordion when there is no one --- apps/web/app/[locale]/settings/team/page.tsx | 10 +++++++--- apps/web/lib/components/accordian.tsx | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/web/app/[locale]/settings/team/page.tsx b/apps/web/app/[locale]/settings/team/page.tsx index 9044cd372..1fcee8c6d 100644 --- a/apps/web/app/[locale]/settings/team/page.tsx +++ b/apps/web/app/[locale]/settings/team/page.tsx @@ -5,8 +5,8 @@ import { Card } from 'lib/components'; import { DangerZoneTeam, TeamAvatar, TeamSettingForm } from 'lib/settings'; -import { useIsMemberManager, useOrganizationTeams } from '@app/hooks'; -import { userState } from '@app/stores'; +import { useIsMemberManager, useOrganizationTeams, useTeamInvitations } from '@app/hooks'; +import { fetchingTeamInvitationsState, userState } from '@app/stores'; import NoTeam from '@components/pages/main/no-team'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; @@ -26,6 +26,9 @@ const Team = () => { const [user] = useRecoilState(userState); const { isTeamMember, activeTeam } = useOrganizationTeams(); const { isTeamManager } = useIsMemberManager(user); + const { teamInvitations } = useTeamInvitations(); + const [isFetchingTeamInvitations] = useRecoilState(fetchingTeamInvitationsState); + return (
{isTeamMember ? ( @@ -49,10 +52,11 @@ const Team = () => { {/* Invitations */} - {isTeamManager ? ( + {isTeamManager && !isFetchingTeamInvitations ? ( diff --git a/apps/web/lib/components/accordian.tsx b/apps/web/lib/components/accordian.tsx index e0c4ab48f..47602ee1f 100644 --- a/apps/web/lib/components/accordian.tsx +++ b/apps/web/lib/components/accordian.tsx @@ -8,14 +8,15 @@ interface isProps { className: string; isDanger?: boolean; id?: string; + defaultOpen?: boolean; } -export const Accordian = ({ children, title, className, isDanger, id }: isProps) => { +export const Accordian = ({ children, title, className, isDanger, id, defaultOpen = true }: isProps) => { return (
- + {({ open }) => ( <> From 057c9397431d7687dd1f059d246f7c2c1116dc2a Mon Sep 17 00:00:00 2001 From: Serge Muhundu Date: Wed, 10 Jul 2024 15:40:36 +0100 Subject: [PATCH 04/10] Fix target sdk version --- apps/mobile/app.template.json | 183 +++++++++++++++++----------------- 1 file changed, 93 insertions(+), 90 deletions(-) diff --git a/apps/mobile/app.template.json b/apps/mobile/app.template.json index a69f6c58c..f48f6bce9 100644 --- a/apps/mobile/app.template.json +++ b/apps/mobile/app.template.json @@ -1,91 +1,94 @@ -{"name": "$EXPO_PROJECT_SLUG", -"displayName": "$EXPO_PROJECT_NAME", -"expo": { - "name": "$EXPO_PROJECT_NAME", - "slug": "$EXPO_PROJECT_SLUG", - "version": "0.1.0", - "orientation": "portrait", - "icon": "./assets/images/ever-teams-logo.png", - "splash": { - "image": "./assets/images/splash-ever-teams.png", - "resizeMode": "cover", - "backgroundColor": "#ffffff" - }, - "owner": "$EXPO_PROJECT_OWNER", - "updates": { - "fallbackToCacheTimeout": 0, - "url": "https://u.expo.dev/$EXPO_PROJECT_ID" - }, - "jsEngine": "hermes", - "assetBundlePatterns": ["**/*"], - "plugins": [ - [ - "expo-media-library", - { - "photosPermission": "Allow $(PRODUCT_NAME) to access your photos.", - "savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos.", - "isAccessMediaLocationEnabled": true - } - ], - "sentry-expo", - [ - "expo-build-properties", - { - "android": { - "enableProguardInReleaseBuilds": true, - "extraProguardRules": "-keep public class com.horcrux.svg.** {*;}", - "allowBackup": false - } - } - ] - ], - "android": { - "icon": "./assets/images/app-icon-android-legacy-ever-teams.png", - "package": "ever.team", - "adaptiveIcon": { - "foregroundImage": "./assets/images/app-icon-android-adaptive-foreground-ever.png", - "backgroundImage": "./assets/images/app-icon-android-adaptive-background.png" - }, - "splash": { - "image": "./assets/images/splash-ever-teams.png", - "resizeMode": "cover", - "backgroundColor": "#ffffff" - }, - "permissions": [ - "android.permission.READ_EXTERNAL_STORAGE", - "android.permission.WRITE_EXTERNAL_STORAGE", - "android.permission.ACCESS_MEDIA_LOCATION" - ] - }, - "ios": { - "icon": "./assets/images/app-icon-ios-ever-teams.png", - "supportsTablet": true, - "bundleIdentifier": "co.ever.teams", - "splash": { - "image": "./assets/images/splash-ever-teams.png", - "tabletImage": "./assets/images/splash-logo-ever-teams-ios-tablet.png", - "resizeMode": "cover", - "backgroundColor": "#ffffff" - }, - "infoPlist": { - "NSCameraUsageDescription": "This app uses the camera to scan barcodes on event tickets.", - "NSPhotoLibraryUsageDescription": "Allow $(PRODUCT_NAME) to access your photos.", - "NSPhotoLibraryAddUsageDescription": "Allow $(PRODUCT_NAME) to save photos." - } - }, - "web": { - "favicon": "./assets/images/app-icon-web-favicon.png", - "splash": { - "image": "./assets/images/splash-logo-web-ever-teams.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - } - }, - "extra": { - "eas": { - "projectId": "$EXPO_PROJECT_ID" - } - }, - "runtimeVersion": "exposdk:48.0.0" -} +{ + "name": "$EXPO_PROJECT_SLUG", + "displayName": "$EXPO_PROJECT_NAME", + "expo": { + "name": "$EXPO_PROJECT_NAME", + "slug": "$EXPO_PROJECT_SLUG", + "version": "0.1.0", + "orientation": "portrait", + "icon": "./assets/images/ever-teams-logo.png", + "splash": { + "image": "./assets/images/splash-ever-teams.png", + "resizeMode": "cover", + "backgroundColor": "#ffffff" + }, + "owner": "$EXPO_PROJECT_OWNER", + "updates": { + "fallbackToCacheTimeout": 0, + "url": "https://u.expo.dev/$EXPO_PROJECT_ID" + }, + "jsEngine": "hermes", + "assetBundlePatterns": ["**/*"], + "plugins": [ + [ + "expo-media-library", + { + "photosPermission": "Allow $(PRODUCT_NAME) to access your photos.", + "savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos.", + "isAccessMediaLocationEnabled": true + } + ], + "sentry-expo", + [ + "expo-build-properties", + { + "android": { + "enableProguardInReleaseBuilds": true, + "extraProguardRules": "-keep public class com.horcrux.svg.** {*;}", + "allowBackup": false, + "minSdkVersion": 23, + "targetSdkVersion": 34 + } + } + ] + ], + "android": { + "icon": "./assets/images/app-icon-android-legacy-ever-teams.png", + "package": "ever.team", + "adaptiveIcon": { + "foregroundImage": "./assets/images/app-icon-android-adaptive-foreground-ever.png", + "backgroundImage": "./assets/images/app-icon-android-adaptive-background.png" + }, + "splash": { + "image": "./assets/images/splash-ever-teams.png", + "resizeMode": "cover", + "backgroundColor": "#ffffff" + }, + "permissions": [ + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.ACCESS_MEDIA_LOCATION" + ] + }, + "ios": { + "icon": "./assets/images/app-icon-ios-ever-teams.png", + "supportsTablet": true, + "bundleIdentifier": "co.ever.teams", + "splash": { + "image": "./assets/images/splash-ever-teams.png", + "tabletImage": "./assets/images/splash-logo-ever-teams-ios-tablet.png", + "resizeMode": "cover", + "backgroundColor": "#ffffff" + }, + "infoPlist": { + "NSCameraUsageDescription": "This app uses the camera to scan barcodes on event tickets.", + "NSPhotoLibraryUsageDescription": "Allow $(PRODUCT_NAME) to access your photos.", + "NSPhotoLibraryAddUsageDescription": "Allow $(PRODUCT_NAME) to save photos." + } + }, + "web": { + "favicon": "./assets/images/app-icon-web-favicon.png", + "splash": { + "image": "./assets/images/splash-logo-web-ever-teams.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + } + }, + "extra": { + "eas": { + "projectId": "$EXPO_PROJECT_ID" + } + }, + "runtimeVersion": "exposdk:48.0.0" + } } From 2779c5ced5301b27a90783f604f494b5e28a171a Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 11 Jul 2024 03:49:26 +0200 Subject: [PATCH 05/10] fix: resizable components hidden tabs and filters on profile page --- apps/web/app/[locale]/profile/[memberId]/page.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index 6a500ece2..3e3dd3ebf 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -29,6 +29,8 @@ export type FilterTab = 'Tasks' | 'Screenshots' | 'Apps' | 'Visited Sites'; const Profile = React.memo(function ProfilePage({ params }: { params: { memberId: string } }) { const profile = useUserProfilePage(); + const [headerSize, setHeaderSize] = useState(10); + const { user } = useAuthenticateUser(); const { isTrackingEnabled, activeTeam, activeTeamManagers } = useOrganizationTeams(); const members = activeTeam?.members; @@ -102,7 +104,12 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId ) : ( - + setHeaderSize(size)} + > Date: Fri, 12 Jul 2024 18:41:05 +0200 Subject: [PATCH 06/10] [Feature] All Teams management (#2699) * feat: load managed teams item for connected user * feat: initialize page for all teams views * feat: load users in different teams * feat: all teams management, find active task for each user for specific team * fix: missing typos cspelling * refact: all teams page folder and files structure * feat: all teams management block view * feat: filter on all teams by user status * feat: empty set component on filter * fix: cspelling typos --- apps/web/app/[locale]/all-teams/component.tsx | 65 +++++++++ apps/web/app/[locale]/all-teams/page.tsx | 5 + apps/web/app/helpers/plan-day-badge.ts | 3 +- .../features/useOrganizationTeamManagers.ts | 44 +++++++ apps/web/app/interfaces/IOrganizationTeam.ts | 6 + apps/web/app/stores/all-teams.ts | 7 + apps/web/app/stores/header-tabs.ts | 5 + .../pages/all-teams/header-tabs.tsx | 46 +++++++ .../features/all-teams-members-block-view.tsx | 38 ++++++ .../features/all-teams-members-card-view.tsx | 44 +++++++ apps/web/lib/features/all-teams-members.tsx | 49 +++++++ .../all-teams/all-team-members-filter.tsx | 124 ++++++++++++++++++ .../users-teams-block/member-block.tsx | 79 +++++++++++ .../all-teams/users-teams-block/user-info.tsx | 8 ++ .../user-team-active-task-times.tsx | 37 ++++++ .../user-team-active-task.tsx | 41 ++++++ .../user-team-task-estimate.tsx | 37 ++++++ .../user-team-today-worked.tsx | 33 +++++ .../users-teams-card/member-infos.tsx | 8 ++ .../all-teams/users-teams-card/user-card.tsx | 92 +++++++++++++ .../user-team-active-task-times.tsx | 30 +++++ .../user-team-active-task.tsx | 35 +++++ .../user-team-task-estimate.tsx | 29 ++++ .../user-team-today-worked.tsx | 12 ++ apps/web/lib/features/team/team-item.tsx | 41 ++++++ .../team/team-outstanding-notifications.tsx | 4 +- apps/web/lib/features/team/teams-dropdown.tsx | 8 +- apps/web/locales/ar.json | 3 + apps/web/locales/bg.json | 3 + apps/web/locales/de.json | 3 + apps/web/locales/en.json | 3 + apps/web/locales/es.json | 3 + apps/web/locales/fr.json | 3 + apps/web/locales/he.json | 3 + apps/web/locales/it.json | 3 + apps/web/locales/nl.json | 3 + apps/web/locales/pl.json | 3 + apps/web/locales/pt.json | 3 + apps/web/locales/ru.json | 3 + apps/web/locales/zh.json | 3 + 40 files changed, 964 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/[locale]/all-teams/component.tsx create mode 100644 apps/web/app/[locale]/all-teams/page.tsx create mode 100644 apps/web/app/hooks/features/useOrganizationTeamManagers.ts create mode 100644 apps/web/app/stores/all-teams.ts create mode 100644 apps/web/components/pages/all-teams/header-tabs.tsx create mode 100644 apps/web/lib/features/all-teams-members-block-view.tsx create mode 100644 apps/web/lib/features/all-teams-members-card-view.tsx create mode 100644 apps/web/lib/features/all-teams-members.tsx create mode 100644 apps/web/lib/features/all-teams/all-team-members-filter.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-block/member-block.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-block/user-info.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-block/user-team-active-task-times.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-block/user-team-active-task.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-block/user-team-task-estimate.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-block/user-team-today-worked.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-card/member-infos.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-card/user-card.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-card/user-team-active-task-times.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-card/user-team-task-estimate.tsx create mode 100644 apps/web/lib/features/all-teams/users-teams-card/user-team-today-worked.tsx diff --git a/apps/web/app/[locale]/all-teams/component.tsx b/apps/web/app/[locale]/all-teams/component.tsx new file mode 100644 index 000000000..544c4f9b4 --- /dev/null +++ b/apps/web/app/[locale]/all-teams/component.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useRecoilValue } from 'recoil'; +import { fullWidthState } from '@app/stores/fullWidth'; +import { withAuthentication } from 'lib/app/authenticator'; +import { Breadcrumb, Container } from 'lib/components'; +import { MainHeader, MainLayout } from 'lib/layout'; +import { useOrganizationAndTeamManagers } from '@app/hooks/features/useOrganizationTeamManagers'; +import { useEffect } from 'react'; +import { useTranslations } from 'next-intl'; +import TeamMemberHeader from 'lib/features/team-member-header'; +import { IssuesView } from '@app/constants'; +import { HeaderTabs } from '@components/pages/all-teams/header-tabs'; +import { allTeamsHeaderTabs } from '@app/stores/header-tabs'; +import AllTeamsMembers from 'lib/features/all-teams-members'; +import { MemberFilter } from 'lib/features/all-teams/all-team-members-filter'; + +function AllTeamsPage() { + const t = useTranslations(); + const fullWidth = useRecoilValue(fullWidthState); + const view = useRecoilValue(allTeamsHeaderTabs); + const { filteredTeams, userManagedTeams } = useOrganizationAndTeamManagers(); + + const breadcrumb = [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: t('common.ALL_TEAMS'), href: '/all-teams' } + ]; + + /* If the user is not a manager in any team or if he's + manager in only one team, then redirect him to the home page + */ + if (userManagedTeams.length < 2) return ; + + return ( + + + {/* Breadcrumb */} +
+ +
+
+ +
+ +
+
+ +
+ + + +
+ ); +} + +function RedirectUser() { + const router = useRouter(); + useEffect(() => { + router.push('/'); + }, [router]); + return <>; +} + +export default withAuthentication(AllTeamsPage, { displayName: 'AllManagedTeams' }); diff --git a/apps/web/app/[locale]/all-teams/page.tsx b/apps/web/app/[locale]/all-teams/page.tsx new file mode 100644 index 000000000..7eb229d44 --- /dev/null +++ b/apps/web/app/[locale]/all-teams/page.tsx @@ -0,0 +1,5 @@ +import AllTeamsPage from './component'; + +export default function Page() { + return ; +} diff --git a/apps/web/app/helpers/plan-day-badge.ts b/apps/web/app/helpers/plan-day-badge.ts index b40497c5d..e5d9322da 100644 --- a/apps/web/app/helpers/plan-day-badge.ts +++ b/apps/web/app/helpers/plan-day-badge.ts @@ -16,7 +16,8 @@ export const planBadgeContent = (plans: IDailyPlan[], taskId: ITeamTask['id']): if (otherPlansWithTask.length > 0) { return 'Planned'; } else { - return `Planned ${formatDayPlanDate(plan.date, 'DD MMM YYYY')}`; + return `${formatDayPlanDate(plan.date, 'DD MMM YYYY')}`; + // return `Planned ${formatDayPlanDate(plan.date, 'DD MMM YYYY')}`; } // The task does not exist in any plan } else { diff --git a/apps/web/app/hooks/features/useOrganizationTeamManagers.ts b/apps/web/app/hooks/features/useOrganizationTeamManagers.ts new file mode 100644 index 000000000..edaab309e --- /dev/null +++ b/apps/web/app/hooks/features/useOrganizationTeamManagers.ts @@ -0,0 +1,44 @@ +import { useRecoilValue } from 'recoil'; +import { useAuthenticateUser } from './useAuthenticateUser'; +import { useOrganizationTeams } from './useOrganizationTeams'; +import { filterValue } from '@app/stores/all-teams'; + +export function useOrganizationAndTeamManagers() { + const { user } = useAuthenticateUser(); + const { teams } = useOrganizationTeams(); + const { value: filtered } = useRecoilValue(filterValue); + + const userManagedTeams = teams.filter((team) => + team.members.some((member) => member.employee?.user?.id === user?.id && member.role?.name === 'MANAGER') + ); + + const filteredTeams = + filtered === 'all' + ? userManagedTeams + : filtered === 'pause' + ? userManagedTeams.map((team) => ({ + ...team, + members: team.members.filter((member) => member.timerStatus === 'pause') + })) + : filtered === 'running' + ? userManagedTeams.map((team) => ({ + ...team, + members: team.members.filter((member) => member.timerStatus === 'running') + })) + : filtered === 'suspended' + ? userManagedTeams.map((team) => ({ + ...team, + members: team.members.filter((member) => member.timerStatus === 'suspended') + })) + : filtered === 'invited' + ? userManagedTeams.map((team) => ({ + ...team, + members: team.members.filter((member) => member.employee.acceptDate) + })) + : userManagedTeams; + + return { + userManagedTeams, + filteredTeams + }; +} diff --git a/apps/web/app/interfaces/IOrganizationTeam.ts b/apps/web/app/interfaces/IOrganizationTeam.ts index 2c12fa8e9..fdb5e455a 100644 --- a/apps/web/app/interfaces/IOrganizationTeam.ts +++ b/apps/web/app/interfaces/IOrganizationTeam.ts @@ -109,6 +109,12 @@ export interface OT_Role { isSystem: boolean; } +export interface ITeamsMembersFilter { + label: string; + value: ITimerStatusEnum | 'all' | 'invited'; + bg: string; +} + export enum RoleNameEnum { SUPER_ADMIN = 'SUPER_ADMIN', ADMIN = 'ADMIN', diff --git a/apps/web/app/stores/all-teams.ts b/apps/web/app/stores/all-teams.ts new file mode 100644 index 000000000..3a9db262b --- /dev/null +++ b/apps/web/app/stores/all-teams.ts @@ -0,0 +1,7 @@ +import { ITeamsMembersFilter } from '@app/interfaces'; +import { atom } from 'recoil'; + +export const filterValue = atom({ + key: 'allTeamsFilterValue', + default: { label: 'All', value: 'all', bg: 'transparent' } +}); diff --git a/apps/web/app/stores/header-tabs.ts b/apps/web/app/stores/header-tabs.ts index 72f9d1f04..3b5d13f08 100644 --- a/apps/web/app/stores/header-tabs.ts +++ b/apps/web/app/stores/header-tabs.ts @@ -5,3 +5,8 @@ export const headerTabs = atom({ key: 'headerTabs', default: IssuesView.CARDS }); + +export const allTeamsHeaderTabs = atom({ + key: 'allTeamsHeaderTabs', + default: IssuesView.CARDS +}); diff --git a/apps/web/components/pages/all-teams/header-tabs.tsx b/apps/web/components/pages/all-teams/header-tabs.tsx new file mode 100644 index 000000000..b3042ced4 --- /dev/null +++ b/apps/web/components/pages/all-teams/header-tabs.tsx @@ -0,0 +1,46 @@ +import { DottedLanguageObjectStringPaths, useTranslations } from 'next-intl'; +import { useRecoilState } from 'recoil'; +import { QueueListIcon, Squares2X2Icon } from '@heroicons/react/20/solid'; +import { IssuesView } from '@app/constants'; +import { allTeamsHeaderTabs } from '@app/stores/header-tabs'; +import { Tooltip } from 'lib/components'; +import { clsxm } from '@app/utils'; + +export function HeaderTabs() { + const t = useTranslations(); + const options = [ + { label: 'TEAMS', icon: QueueListIcon, view: IssuesView.CARDS }, + { label: 'COMBINE', icon: Squares2X2Icon, view: IssuesView.BLOCKS } + ]; + + const [view, setView] = useRecoilState(allTeamsHeaderTabs); + + return ( + <> + {options.map(({ label, icon: Icon, view: optionView }) => ( + + + + ))} + + ); +} diff --git a/apps/web/lib/features/all-teams-members-block-view.tsx b/apps/web/lib/features/all-teams-members-block-view.tsx new file mode 100644 index 000000000..7c6935d64 --- /dev/null +++ b/apps/web/lib/features/all-teams-members-block-view.tsx @@ -0,0 +1,38 @@ +import { IOrganizationTeamList, OT_Member } from '@app/interfaces'; +import UserTeamBlockCard from './all-teams/users-teams-block/member-block'; + +interface Employee extends OT_Member { + teams: { team: IOrganizationTeamList; activeTaskId?: string | null }[]; +} + +export default function AllTeamsMembersBlockView({ teams }: { teams: IOrganizationTeamList[] }) { + const employees: Employee[] = teams.flatMap((team) => + team.members.map((member) => ({ + ...member, + teams: [{ team, activeTaskId: member.activeTaskId }] + })) + ); + + const groupedEmployees: Record = employees.reduce( + (acc, employee) => { + if (!acc[employee.employeeId]) { + acc[employee.employeeId] = { ...employee, teams: [] }; + } + acc[employee.employeeId].teams.push(...employee.teams); + return acc; + }, + {} as Record + ); + + const employeesArray: Employee[] = Object.values(groupedEmployees); + + return ( + <> + {employeesArray.length > 0 ? ( + employeesArray.map((employee) => ) + ) : ( +
There is no member for filtered value
+ )} + + ); +} diff --git a/apps/web/lib/features/all-teams-members-card-view.tsx b/apps/web/lib/features/all-teams-members-card-view.tsx new file mode 100644 index 000000000..e9f8e47e6 --- /dev/null +++ b/apps/web/lib/features/all-teams-members-card-view.tsx @@ -0,0 +1,44 @@ +import { IOrganizationTeamList } from '@app/interfaces'; +import UserTeamCard from './all-teams/users-teams-card/user-card'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; +import { HorizontalSeparator } from 'lib/components'; + +export default function TeamsMembersCardView({ teams }: { teams: IOrganizationTeamList[] }) { + return ( +
+ t.name)}> + {teams.map((team) => { + return ( + + +
+ + {team.name} ({team.members.length}) + + + {/* */} +
+
+ + + {team.members.length > 0 ? ( + team.members.map((member) => { + return ; + }) + ) : ( +
+ There is no member for filtered value in the team{' '} + + {' '} + {team.name} + +
+ )} +
+
+ ); + })} +
+
+ ); +} diff --git a/apps/web/lib/features/all-teams-members.tsx b/apps/web/lib/features/all-teams-members.tsx new file mode 100644 index 000000000..a08034089 --- /dev/null +++ b/apps/web/lib/features/all-teams-members.tsx @@ -0,0 +1,49 @@ +import { useRecoilValue } from 'recoil'; +import { IssuesView } from '@app/constants'; +import { IOrganizationTeamList } from '@app/interfaces'; +import { fullWidthState } from '@app/stores/fullWidth'; +import { Container } from 'lib/components'; +import UserTeamCardSkeletonCard from '@components/shared/skeleton/UserTeamCardSkeleton'; +import InviteUserTeamCardSkeleton from '@components/shared/skeleton/InviteTeamCardSkeleton'; +import { UserCard } from '@components/shared/skeleton/TeamPageSkeleton'; +import TeamsMembersCardView from './all-teams-members-card-view'; +import AllTeamsMembersBlockView from './all-teams-members-block-view'; + +export default function AllTeamsMembers({ + teams, + view = IssuesView.CARDS +}: { + teams: IOrganizationTeamList[]; + view: IssuesView; +}) { + const fullWidth = useRecoilValue(fullWidthState); + let teamsMembersView; + + switch (true) { + case teams.length === 0: + teamsMembersView = ( + +
+ + +
+
+ + + +
+
+ ); + break; + case view === IssuesView.CARDS: + teamsMembersView = ; + break; + case view === IssuesView.BLOCKS: + teamsMembersView = ; + break; + default: + teamsMembersView = ; + } + + return teamsMembersView; +} diff --git a/apps/web/lib/features/all-teams/all-team-members-filter.tsx b/apps/web/lib/features/all-teams/all-team-members-filter.tsx new file mode 100644 index 000000000..8d1daa057 --- /dev/null +++ b/apps/web/lib/features/all-teams/all-team-members-filter.tsx @@ -0,0 +1,124 @@ +import { ITeamsMembersFilter } from '@app/interfaces'; +import { filterValue } from '@app/stores/all-teams'; +import { clsxm } from '@app/utils'; +import { Listbox, Transition } from '@headlessui/react'; +import { ChevronDownIcon, CircleIcon } from 'assets/svg'; +import { Card, Tooltip } from 'lib/components'; +import { Fragment, PropsWithChildren } from 'react'; +import { useRecoilState } from 'recoil'; + +export function MemberFilterOption({ + children, + label, + active = true, + checked = false, + showIcon = true, + icon, + bg +}: PropsWithChildren<{ + label: string; + active?: boolean; + checked?: boolean; + showIcon?: boolean; + icon?: React.ReactNode; + bg?: string; +}>) { + return ( +
+
+ {checked ? ( + + + + ) : ( + <>{showIcon && active && icon} + )} +
{label}
+
+ {children} +
+ ); +} + +export function MemberFilter() { + const options: ITeamsMembersFilter[] = [ + { label: 'All', value: 'all', bg: 'transparent' }, + { label: 'Working now', value: 'running', bg: '#1f973d33' }, + { label: 'Paused', value: 'pause', bg: '#e58484' }, + { label: 'Off', value: 'suspended', bg: '#6b7280' }, + { label: 'Invited', value: 'invited', bg: '#d1ad5b' } + ]; + const [value, setValue] = useRecoilState(filterValue); + + return ( + +
+ setValue(v)}> + {({ open }) => { + return ( + <> + + + + + } + bg={value.bg} + > + + + + + + + {options.map((item) => ( + +
  • + + + + } + /> +
  • +
    + ))} +
    +
    +
    + + ); + }} +
    +
    +
    + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-block/member-block.tsx b/apps/web/lib/features/all-teams/users-teams-block/member-block.tsx new file mode 100644 index 000000000..c2cffe9ae --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-block/member-block.tsx @@ -0,0 +1,79 @@ +import { useTimer } from '@app/hooks'; +import { IOrganizationTeamList, ITimerStatusEnum, OT_Member } from '@app/interfaces'; +import { clsxm } from '@app/utils'; +import { Card, HorizontalSeparator } from 'lib/components'; +import { getTimerStatusValue } from 'lib/features/timer/timer-status'; +import { useMemo } from 'react'; +import MemberBoxInfo from './user-info'; +import { BlockCardMemberTodayWorked } from './user-team-today-worked'; +import UserTeamActiveBlockTaskInfo from './user-team-active-task'; +import UserTeamActiveTaskTimesBlock from './user-team-active-task-times'; +import UserTeamActiveTaskEstimateBlock from './user-team-task-estimate'; + +const cardColorType = { + running: ' border-green-300', + idle: ' border-[#F5BEBE]', + online: ' border-green-300', + pause: ' border-[#EFCF9E]', + suspended: ' border-[#DCD6D6]' +}; + +interface Member extends OT_Member { + teams: { team: IOrganizationTeamList; activeTaskId?: string | null }[]; +} + +export default function UserTeamBlockCard({ member }: { member: Member }) { + const { timerStatus } = useTimer(); + + const timerStatusValue: ITimerStatusEnum = useMemo(() => { + return getTimerStatusValue(timerStatus, member, true); + }, [timerStatus, member]); + + return ( +
    + +
    + + {/* total time */} +
    + + {/*
    {menu}
    */} +
    +
    + + + + <> + {member.teams.map((team) => ( +
    + <> +
    {team.team.name}
    + + + +
    +
    + +
    + +
    + +
    + ))} + +
    +
    + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-block/user-info.tsx b/apps/web/lib/features/all-teams/users-teams-block/user-info.tsx new file mode 100644 index 000000000..5910c8b5a --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-block/user-info.tsx @@ -0,0 +1,8 @@ +import { useTeamMemberCard } from '@app/hooks'; +import { OT_Member } from '@app/interfaces'; +import { UserBoxInfo } from 'lib/features/team/user-team-block/user-info'; + +export default function MemberBoxInfo({ member }: { member: OT_Member }) { + const memberInfo = useTeamMemberCard(member); + return ; +} diff --git a/apps/web/lib/features/all-teams/users-teams-block/user-team-active-task-times.tsx b/apps/web/lib/features/all-teams/users-teams-block/user-team-active-task-times.tsx new file mode 100644 index 000000000..5a1cf6ae8 --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-block/user-team-active-task-times.tsx @@ -0,0 +1,37 @@ +import { useTeamMemberCard, useTeamTasks } from '@app/hooks'; +import { ITeamTask, OT_Member } from '@app/interfaces'; +import { TaskTimes } from 'lib/features/task/task-times'; +import { useEffect, useState } from 'react'; + +export default function UserTeamActiveTaskTimesBlock({ + member, + activeTaskId +}: { + member: OT_Member; + activeTaskId: string; +}) { + const memberInfo = useTeamMemberCard(member); + + const { getTaskById } = useTeamTasks(); + + const [activeTask, setActiveTask] = useState(null); + + useEffect(() => { + getTaskById(activeTaskId || '') + .then((response) => setActiveTask(response.data)) + .catch((_) => console.log(_)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-block/user-team-active-task.tsx b/apps/web/lib/features/all-teams/users-teams-block/user-team-active-task.tsx new file mode 100644 index 000000000..aa8126348 --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-block/user-team-active-task.tsx @@ -0,0 +1,41 @@ +import { useTeamMemberCard, useTeamTasks, useTMCardTaskEdit } from '@app/hooks'; +import { ITeamTask, OT_Member } from '@app/interfaces'; +import { TaskBlockInfo } from 'lib/features/team/user-team-block/task-info'; +import { useEffect, useState } from 'react'; + +export default function UserTeamActiveBlockTaskInfo({ + member, + activeTaskId +}: { + member: OT_Member; + activeTaskId: string; +}) { + const memberInfo = useTeamMemberCard(member); + const [activeTask, setActiveTask] = useState(null); + const taskEdition = useTMCardTaskEdit(activeTask); + + const { getTaskById } = useTeamTasks(); + + useEffect(() => { + getTaskById(activeTaskId || '') + .then((response) => setActiveTask(response.data)) + .catch((_) => console.log(_)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + {activeTask?.id ? ( + + ) : ( +
    No active task
    + )} + + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-block/user-team-task-estimate.tsx b/apps/web/lib/features/all-teams/users-teams-block/user-team-task-estimate.tsx new file mode 100644 index 000000000..e5334182d --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-block/user-team-task-estimate.tsx @@ -0,0 +1,37 @@ +import { useTeamMemberCard, useTeamTasks, useTMCardTaskEdit } from '@app/hooks'; +import { ITeamTask, OT_Member } from '@app/interfaces'; +import { TaskEstimateInfo } from 'lib/features/team/user-team-card/task-estimate'; +import { useEffect, useState } from 'react'; + +export default function UserTeamActiveTaskEstimateBlock({ + member, + activeTaskId +}: { + member: OT_Member; + activeTaskId: string; +}) { + const memberInfo = useTeamMemberCard(member); + const [activeTask, setActiveTask] = useState(null); + const taskEdition = useTMCardTaskEdit(activeTask); + + const { getTaskById } = useTeamTasks(); + + useEffect(() => { + getTaskById(activeTaskId || '') + .then((response) => setActiveTask(response.data)) + .catch((_) => console.log(_)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-block/user-team-today-worked.tsx b/apps/web/lib/features/all-teams/users-teams-block/user-team-today-worked.tsx new file mode 100644 index 000000000..dbe12596a --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-block/user-team-today-worked.tsx @@ -0,0 +1,33 @@ +import { secondsToTime } from '@app/helpers'; +import { useTaskStatistics, useTeamMemberCard } from '@app/hooks'; +import { OT_Member } from '@app/interfaces'; +import { timerSecondsState } from '@app/stores'; +import { clsxm } from '@app/utils'; +import { Text } from 'lib/components'; +import { useTranslations } from 'next-intl'; +import { useRecoilValue } from 'recoil'; + +export function BlockCardMemberTodayWorked({ member }: { member: OT_Member }) { + const t = useTranslations(); + const memberInfo = useTeamMemberCard(member); + + const seconds = useRecoilValue(timerSecondsState); + const { activeTaskTotalStat, addSeconds } = useTaskStatistics(seconds); + + const { h, m } = secondsToTime( + ((member?.totalTodayTasks && + member?.totalTodayTasks.reduce( + (previousValue, currentValue) => previousValue + currentValue.duration, + 0 + )) || + activeTaskTotalStat?.duration || + 0) + addSeconds + ); + + return ( +
    + {t('common.TOTAL_WORKED_TODAY')} + {memberInfo.isAuthUser ? `${h}h : ${m}m` : `0h : 0m`} +
    + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-card/member-infos.tsx b/apps/web/lib/features/all-teams/users-teams-card/member-infos.tsx new file mode 100644 index 000000000..a43b32dfd --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-card/member-infos.tsx @@ -0,0 +1,8 @@ +import { useTeamMemberCard } from '@app/hooks'; +import { OT_Member } from '@app/interfaces'; +import { UserInfo } from 'lib/features/team/user-team-card/user-info'; + +export default function MemberInfo({ member }: { member: OT_Member }) { + const memberInfo = useTeamMemberCard(member); + return ; +} diff --git a/apps/web/lib/features/all-teams/users-teams-card/user-card.tsx b/apps/web/lib/features/all-teams/users-teams-card/user-card.tsx new file mode 100644 index 000000000..3957a7797 --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-card/user-card.tsx @@ -0,0 +1,92 @@ +import { ITeamTask, OT_Member } from '@app/interfaces'; +import { clsxm } from '@app/utils'; +import { Transition } from '@headlessui/react'; +import { SixSquareGridIcon } from 'assets/svg'; +import { Card, VerticalSeparator } from 'lib/components'; +import MemberInfo from './member-infos'; +import UserTeamActiveTaskInfo from './user-team-active-task'; +import UserTeamActiveTaskTimes from './user-team-active-task-times'; +import UserTeamActiveTaskEstimate from './user-team-task-estimate'; +import UserTeamActiveTaskTodayWorked from './user-team-today-worked'; +import { UserTeamCardMenu } from 'lib/features/team/user-team-card/user-team-card-menu'; +import { useTeamMemberCard, useTeamTasks, useTMCardTaskEdit } from '@app/hooks'; +import { useEffect, useState } from 'react'; + +export default function UserTeamCard({ member }: { member: OT_Member }) { + return ( + + + + ); +} + +function UserActiveTaskMenu({ member }: { member: OT_Member }) { + const memberInfo = useTeamMemberCard(member); + const [activeTask, setActiveTask] = useState(null); + const taskEdition = useTMCardTaskEdit(activeTask); + + const { getTaskById } = useTeamTasks(); + + useEffect(() => { + getTaskById(member.activeTaskId || '') + .then((response) => setActiveTask(response.data)) + .catch((_) => console.log(_)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( + <> + + + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task-times.tsx b/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task-times.tsx new file mode 100644 index 000000000..53545b014 --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task-times.tsx @@ -0,0 +1,30 @@ +import { useTeamMemberCard, useTeamTasks } from '@app/hooks'; +import { ITeamTask, OT_Member } from '@app/interfaces'; +import { TaskTimes } from 'lib/features/task/task-times'; +import { useEffect, useState } from 'react'; + +export default function UserTeamActiveTaskTimes({ member }: { member: OT_Member }) { + const memberInfo = useTeamMemberCard(member); + + const { getTaskById } = useTeamTasks(); + + const [activeTask, setActiveTask] = useState(null); + + useEffect(() => { + getTaskById(member.activeTaskId || '') + .then((response) => setActiveTask(response.data)) + .catch((_) => console.log(_)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx b/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx new file mode 100644 index 000000000..ded3e360e --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx @@ -0,0 +1,35 @@ +import { useTeamMemberCard, useTeamTasks, useTMCardTaskEdit } from '@app/hooks'; +import { ITeamTask, OT_Member } from '@app/interfaces'; +import { TaskInfo } from 'lib/features/team/user-team-card/task-info'; +import { useEffect, useState } from 'react'; + +export default function UserTeamActiveTaskInfo({ member }: { member: OT_Member }) { + const memberInfo = useTeamMemberCard(member); + const [activeTask, setActiveTask] = useState(null); + const taskEdition = useTMCardTaskEdit(activeTask); + + const { getTaskById } = useTeamTasks(); + + useEffect(() => { + getTaskById(member.activeTaskId || '') + .then((response) => setActiveTask(response.data)) + .catch((_) => console.log(_)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + {activeTask?.id ? ( + + ) : ( +
    --
    + )} + + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-card/user-team-task-estimate.tsx b/apps/web/lib/features/all-teams/users-teams-card/user-team-task-estimate.tsx new file mode 100644 index 000000000..e61f1702e --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-card/user-team-task-estimate.tsx @@ -0,0 +1,29 @@ +import { useTeamMemberCard, useTeamTasks, useTMCardTaskEdit } from '@app/hooks'; +import { ITeamTask, OT_Member } from '@app/interfaces'; +import { TaskEstimateInfo } from 'lib/features/team/user-team-card/task-estimate'; +import { useEffect, useState } from 'react'; + +export default function UserTeamActiveTaskEstimate({ member }: { member: OT_Member }) { + const memberInfo = useTeamMemberCard(member); + const [activeTask, setActiveTask] = useState(null); + const taskEdition = useTMCardTaskEdit(activeTask); + + const { getTaskById } = useTeamTasks(); + + useEffect(() => { + getTaskById(member.activeTaskId || '') + .then((response) => setActiveTask(response.data)) + .catch((_) => console.log(_)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +} diff --git a/apps/web/lib/features/all-teams/users-teams-card/user-team-today-worked.tsx b/apps/web/lib/features/all-teams/users-teams-card/user-team-today-worked.tsx new file mode 100644 index 000000000..c2319e801 --- /dev/null +++ b/apps/web/lib/features/all-teams/users-teams-card/user-team-today-worked.tsx @@ -0,0 +1,12 @@ +import { useTeamMemberCard } from '@app/hooks'; +import { OT_Member } from '@app/interfaces'; +import { TodayWorkedTime } from 'lib/features/task/task-times'; + +export default function UserTeamActiveTaskTodayWorked({ member }: { member: OT_Member }) { + const memberInfo = useTeamMemberCard(member); + return ( +
    + +
    + ); +} diff --git a/apps/web/lib/features/team/team-item.tsx b/apps/web/lib/features/team/team-item.tsx index 98373b8da..636a64557 100644 --- a/apps/web/lib/features/team/team-item.tsx +++ b/apps/web/lib/features/team/team-item.tsx @@ -152,3 +152,44 @@ export function TeamItem({
    ); } + +export function AllTeamItem({ title, count }: { title: string; count: number }) { + return ( + +
    +
    +
    + {imgTitle(title)} +
    +
    +
    + + {title} + + + {count ? `(${count})` : ''} + +
    +
    + + ); +} diff --git a/apps/web/lib/features/team/team-outstanding-notifications.tsx b/apps/web/lib/features/team/team-outstanding-notifications.tsx index c172d1ccb..991e45736 100644 --- a/apps/web/lib/features/team/team-outstanding-notifications.tsx +++ b/apps/web/lib/features/team/team-outstanding-notifications.tsx @@ -22,8 +22,6 @@ export function TeamOutstandingNotifications() { getEmployeeDayPlans(user?.employee.id || ''); }, [getAllDayPlans, getEmployeeDayPlans, user?.employee.id]); - console.log(dailyPlan); - return (
    {outstandingPlans && outstandingPlans.length > 0 && ( @@ -174,7 +172,7 @@ function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstanding
    {t('pages.home.OUTSTANDING_NOTIFICATIONS.SUBJECT')} {uniqueEmployees?.length} team member(s) - with unconpleted tasks, please see{' '} + with uncompleted tasks, please see{' '} {uniqueEmployees?.map((em) => ( { const { user } = useAuthenticateUser(); const { teams, activeTeam, setActiveTeam } = useOrganizationTeams(); + const { userManagedTeams } = useOrganizationAndTeamManagers(); const { timerStatus, stopTimer } = useTimer(); const t = useTranslations(); const { toast } = useToast(); @@ -72,6 +74,10 @@ export const TeamsDropDown = ({ publicTeam }: { publicTeam?: boolean }) => { // loading={teamsFetching} // TODO: Enable loading in future when we implement better data fetching library like TanStack publicTeam={publicTeam} > + {userManagedTeams.length > 1 && ( + + )} + {!publicTeam && ( Date: Fri, 12 Jul 2024 18:41:27 +0200 Subject: [PATCH 07/10] chore(deps): bump fast-loops from 1.1.3 to 1.1.4 in /apps/mobile (#2707) Bumps [fast-loops](https://github.com/robinweser/fast-loops) from 1.1.3 to 1.1.4. - [Commits](https://github.com/robinweser/fast-loops/commits) --- updated-dependencies: - dependency-name: fast-loops dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/mobile/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/mobile/yarn.lock b/apps/mobile/yarn.lock index 59df746a7..03cbdb298 100644 --- a/apps/mobile/yarn.lock +++ b/apps/mobile/yarn.lock @@ -6910,9 +6910,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-loops@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.3.tgz#ce96adb86d07e7bf9b4822ab9c6fac9964981f75" - integrity sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g== + version "1.1.4" + resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.4.tgz#61bc77d518c0af5073a638c6d9d5c7683f069ce2" + integrity sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg== fast-xml-parser@^4.0.12: version "4.3.2" From 4be0660ec8e8d3688e6e387c2cb49d96a351ca1f Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:51:32 +0200 Subject: [PATCH 08/10] [Feat] Daily plan | delete (#2700) * fix/dailyplan/deleteplan * fix: dailyplan deleteplan * fix: Typos & errors * fix: cspell Error * fix: daily plan delete today and future task --------- Co-authored-by: Cedric Karungu --- apps/web/lib/components/alert-popup.tsx | 40 ++++++++++++ apps/web/lib/components/index.ts | 1 + .../features/task/daily-plan/future-tasks.tsx | 46 +++++++++++--- .../task/daily-plan/outstanding-date.tsx | 4 +- .../features/task/daily-plan/outstanding.tsx | 6 +- apps/web/lib/features/user-profile-plans.tsx | 61 +++++++++++++------ apps/web/lib/features/user-profile-tasks.tsx | 18 +++--- 7 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 apps/web/lib/components/alert-popup.tsx diff --git a/apps/web/lib/components/alert-popup.tsx b/apps/web/lib/components/alert-popup.tsx new file mode 100644 index 000000000..73442f787 --- /dev/null +++ b/apps/web/lib/components/alert-popup.tsx @@ -0,0 +1,40 @@ +import React from 'react' + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@components/ui/popover" + + +interface IConfirmPopup { + buttonOpen: any, + open: boolean +} +/** + * + * + * @export + * @param {React.PropsWithChildren} { children, buttonOpen, open } + * @return {*} + */ +export function AlertPopup({ children, buttonOpen, open }: React.PropsWithChildren) { + return ( + + + {buttonOpen} + + +
    +
    +

    Delete this plan

    +

    Are you sure you want to delete this plan?

    +
    +
    + {children} +
    +
    +
    +
    + ) +} diff --git a/apps/web/lib/components/index.ts b/apps/web/lib/components/index.ts index f5f1215e6..90dfd5b2b 100644 --- a/apps/web/lib/components/index.ts +++ b/apps/web/lib/components/index.ts @@ -27,3 +27,4 @@ export * from './inputs/auth-code-input'; export * from './services/recaptcha'; export * from './copy-tooltip'; +export * from './alert-popup' diff --git a/apps/web/lib/features/task/daily-plan/future-tasks.tsx b/apps/web/lib/features/task/daily-plan/future-tasks.tsx index 303aeedbe..5cb66f6bf 100644 --- a/apps/web/lib/features/task/daily-plan/future-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/future-tasks.tsx @@ -5,10 +5,13 @@ import { TaskCard } from '../task-card'; import { Button } from '@components/ui/button'; import { useCanSeeActivityScreen, useDailyPlan } from '@app/hooks'; import { ReloadIcon } from '@radix-ui/react-icons'; +import { useState } from 'react'; +import { AlertPopup } from 'lib/components'; export function FutureTasks({ profile }: { profile: any }) { const { deleteDailyPlan, deleteDailyPlanLoading, futurePlans } = useDailyPlan(); const canSeeActivity = useCanSeeActivityScreen(); + const [popupOpen, setPopupOpen] = useState(false) return (
    @@ -55,17 +58,40 @@ export function FutureTasks({ profile }: { profile: any }) { {/* Delete Plan */} {canSeeActivity ? (
    - + } > - {deleteDailyPlanLoading && ( - - )} - Delete this plan - + {/*button confirm*/} + + {/*button cancel*/} + +
    ) : ( <> diff --git a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx index ce94aeb5b..8ff8e07fd 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx @@ -4,10 +4,10 @@ import { EmptyPlans, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; import { useDailyPlan } from '@app/hooks'; -interface IOutstandingFieltreDate { +interface IOutstandingFilterDate { profile: any } -export function OutstandingFieltreDate({ profile }: IOutstandingFieltreDate) { +export function OutstandingFilterDate({ profile }: IOutstandingFilterDate) { const { outstandingPlans } = useDailyPlan(); return (
    diff --git a/apps/web/lib/features/task/daily-plan/outstanding.tsx b/apps/web/lib/features/task/daily-plan/outstanding.tsx index b1b441281..61c3a6898 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding.tsx @@ -1,6 +1,6 @@ interface IOutstanding { - filtre?: any; + filter?: any; } -export function Outstanding({ filtre }: IOutstanding) { - return (<>{filtre}); +export function Outstanding({ filter }: IOutstanding) { + return (<>{filter}); } diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index b7c799b7d..f8063b8c4 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -5,7 +5,7 @@ import { useRecoilValue } from 'recoil'; import { useCanSeeActivityScreen, useDailyPlan, useUserProfilePage } from '@app/hooks'; import { TaskCard } from './task/task-card'; import { IDailyPlan } from '@app/interfaces'; -import { Container, NoData, ProgressBar, VerticalSeparator } from 'lib/components'; +import { AlertPopup, Container, NoData, ProgressBar, VerticalSeparator } from 'lib/components'; import { clsxm } from '@app/utils'; import { fullWidthState } from '@app/stores/fullWidth'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; @@ -13,14 +13,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { formatDayPlanDate, formatIntegerToHour } from '@app/helpers'; import { EditPenBoxIcon, CheckCircleTickIcon as TickSaveIcon } from 'assets/svg'; import { ReaderIcon, ReloadIcon } from '@radix-ui/react-icons'; -import { OutstandingAll, PastTasks, Outstanding, OutstandingFieltreDate } from './task/daily-plan'; +import { OutstandingAll, PastTasks, Outstanding, OutstandingFilterDate } from './task/daily-plan'; import { FutureTasks } from './task/daily-plan/future-tasks'; import { Button } from '@components/ui/button'; import { IoCalendarOutline } from "react-icons/io5"; type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; -type FiltreOutstanding = 'ALL' | 'DATE'; +type FilterOutstanding = 'ALL' | 'DATE'; export function UserProfilePlans() { const defaultTab = @@ -28,26 +28,26 @@ export function UserProfilePlans() { ? (window.localStorage.getItem('daily-plan-tab') as FilterTabs) || null : 'Today Tasks'; - const defaultOutstanding = typeof window !== 'undefined' ? (window.localStorage.getItem('outstanding') as FiltreOutstanding) || null : 'ALL'; + const defaultOutstanding = typeof window !== 'undefined' ? (window.localStorage.getItem('outstanding') as FilterOutstanding) || null : 'ALL'; const profile = useUserProfilePage(); const { todayPlan, futurePlans, pastPlans, outstandingPlans, sortedPlans, profileDailyPlans } = useDailyPlan(); const fullWidth = useRecoilValue(fullWidthState); const [currentTab, setCurrentTab] = useState(defaultTab || 'Today Tasks'); - const [currentOutstanding, setCurrentOutstanding] = useState(defaultOutstanding || 'ALL'); + const [currentOutstanding, setCurrentOutstanding] = useState(defaultOutstanding || 'ALL'); const screenOutstanding = { 'ALL': , - "DATE": + "DATE": } const tabsScreens = { 'Today Tasks': , 'Future Tasks': , 'Past Tasks': , 'All Tasks': , - Outstanding: + Outstanding: }; @@ -98,7 +98,7 @@ export function UserProfilePlans() {
    {currentTab === 'Outstanding' && ( { - setCurrentOutstanding(value as FilterOutstanding) - }}> - - - - - {Object.keys(screenOutstanding).map((item, index) => ( - -
    - {item == 'DATE' && } - {item} -
    -
    - ))} -
    - - )} +
    + + {currentTab === 'Outstanding' && ( + + )} +
    {tabsScreens[currentTab]}
    @@ -131,13 +137,15 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current // Filter plans let filteredPlans: IDailyPlan[] = []; const { deleteDailyPlan, deleteDailyPlanLoading, sortedPlans, todayPlan } = useDailyPlan(); - const [popupOpen, setPopupOpen] = useState(false) + const [popupOpen, setPopupOpen] = useState(false); filteredPlans = sortedPlans; if (currentTab === 'Today Tasks') filteredPlans = todayPlan; const canSeeActivity = useCanSeeActivityScreen(); + const view = useRecoilValue(dailyPlanViewHeaderTabs); + return (
    {filteredPlans?.length > 0 ? ( @@ -154,11 +162,14 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current - -
    - {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + +
    +
    + {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
    +
    @@ -166,22 +177,33 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current {/* Plan tasks list */} -
      - {plan.tasks?.map((task) => ( - - ))} +
        + {plan.tasks?.map((task) => + view === 'CARDS' ? ( + + ) : ( + + ) + )}
      {/* Delete Plan */} @@ -223,7 +245,6 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current Cancel -
    ) : ( <>