From 25e31d863fbf1a75bbdfd81946f9af4842db813a Mon Sep 17 00:00:00 2001 From: Arick Bulakali <85836702+NdekoCode@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:07:24 +0200 Subject: [PATCH] 3108 feature/ task list view all tasks in a team (#3234) * refactor(web): [Task in Team]: Add task table and paginations * refactor(web): [Task in Team]: Add filter and pagination * refactor(web): [Task in Team] Add search feature * fix(web): [Task in Team] search feature * fix(web): fix main container horizontal scroll * refactor(web): replace overflow-scroll by overflow-auto * fix(web): [Vertical Sidebar] fix sidebar trigger on collapsed icons * fix(web): eslint error * fix(web): [Vertical Sidebar] fix active sidebar menu on page change * fix(web): [Vertical Sidebar] fix label on active sidebar menu * refactor(web): [Vertical Sidebar] remove project sub-menu * fix(web): deepscan error * fix(web): deepscan error * refactor(web): [Task in Team] Enhance accessibility and styling consistency for assign-user * refactor(web): Encode username parameter in URL. * refactor(web): [Task in Team] Fix duplication of Status Badges * refactor(web): Simplify the conditional rendering by removing unnecessary Fragment. --- .vscode/settings.json | 2 +- apps/web/app/[locale]/layout.tsx | 230 +++--- apps/web/app/[locale]/page-component.tsx | 10 +- .../app/[locale]/permissions/component.tsx | 566 +++++++-------- .../app/[locale]/profile/[memberId]/page.tsx | 2 +- apps/web/app/[locale]/team/tasks/page.tsx | 39 ++ .../app/hooks/features/useAuthTeamTasks.ts | 112 ++- apps/web/app/interfaces/ITask.ts | 2 +- apps/web/app/layout.tsx | 6 +- apps/web/app/stores/menu.ts | 8 + apps/web/components/app-sidebar.tsx | 282 ++++---- apps/web/components/nav-main.tsx | 127 +++- .../task-description-editor.tsx | 438 ++++++------ .../details-section/blocks/task-main-info.tsx | 654 ++++++++---------- .../pages/team/tasks/AssigneeUser.tsx | 30 + .../pages/team/tasks/DropdownMenuTask.tsx | 92 +++ .../pages/team/tasks/FilterButton.tsx | 97 +++ .../pages/team/tasks/StatusBadge.tsx | 23 + .../components/pages/team/tasks/TaskTable.tsx | 30 + .../components/pages/team/tasks/columns.tsx | 135 ++++ .../pages/team/tasks/tasks-data-table.tsx | 170 +++++ apps/web/components/ui/sidebar.tsx | 24 +- apps/web/components/ui/table.tsx | 24 +- apps/web/lib/components/combobox/index.tsx | 214 +++--- apps/web/lib/components/pagination.tsx | 14 +- apps/web/lib/components/svgs/app-logo.tsx | 9 +- apps/web/lib/components/time-picker/index.tsx | 341 ++++----- .../components/screenshot-details.tsx | 3 +- .../calendar/calendar-component.tsx | 190 ++--- .../features/task/daily-plan/future-tasks.tsx | 352 +++++----- .../task/daily-plan/outstanding-all.tsx | 205 +++--- .../task/daily-plan/outstanding-date.tsx | 236 +++---- .../features/task/daily-plan/past-tasks.tsx | 283 ++++---- apps/web/lib/features/task/task-status.tsx | 4 +- apps/web/lib/features/team-members.tsx | 12 +- apps/web/lib/features/user-profile-plans.tsx | 7 +- apps/web/lib/layout/main-layout.tsx | 2 +- apps/web/locales/en.json | 2 +- apps/web/package.json | 2 +- apps/web/public/assets/user-teams.png | Bin 0 -> 387455 bytes yarn.lock | 18 +- 41 files changed, 2724 insertions(+), 2273 deletions(-) create mode 100644 apps/web/app/[locale]/team/tasks/page.tsx create mode 100644 apps/web/app/stores/menu.ts create mode 100644 apps/web/components/pages/team/tasks/AssigneeUser.tsx create mode 100644 apps/web/components/pages/team/tasks/DropdownMenuTask.tsx create mode 100644 apps/web/components/pages/team/tasks/FilterButton.tsx create mode 100644 apps/web/components/pages/team/tasks/StatusBadge.tsx create mode 100644 apps/web/components/pages/team/tasks/TaskTable.tsx create mode 100644 apps/web/components/pages/team/tasks/columns.tsx create mode 100644 apps/web/components/pages/team/tasks/tasks-data-table.tsx create mode 100644 apps/web/public/assets/user-teams.png diff --git a/.vscode/settings.json b/.vscode/settings.json index 49b03091f..06dae99cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,7 +19,7 @@ "source.organizeImports": "never", "source.sortMembers": "never", "organizeImports": "never", - "source.removeUnusedImports": "always" + // "source.removeUnusedImports": "always" }, "vsicons.presets.angular": true, "deepscan.enable": true, diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx index e2f2876c3..8efdc3b8e 100644 --- a/apps/web/app/[locale]/layout.tsx +++ b/apps/web/app/[locale]/layout.tsx @@ -21,46 +21,28 @@ import { JitsuOptions } from '@jitsu/jitsu-react/dist/useJitsu'; import { PHProvider } from './integration/posthog/provider'; -const locales = [ - 'en', - 'de', - 'ar', - 'bg', - 'zh', - 'nl', - 'de', - 'he', - 'it', - 'pl', - 'pt', - 'ru', - 'es', - 'fr' -]; +const locales = ['en', 'de', 'ar', 'bg', 'zh', 'nl', 'de', 'he', 'it', 'pl', 'pt', 'ru', 'es', 'fr']; interface Props { - params: { locale: string }; - - pageProps: { - jitsuConf?: JitsuOptions; - jitsuHost?: string; - envs: Record; - user?: any; - }; + params: { locale: string }; + + pageProps: { + jitsuConf?: JitsuOptions; + jitsuHost?: string; + envs: Record; + user?: any; + }; } const poppins = Poppins({ - subsets: ['latin'], - weight: '500', - variable: '--font-poppins', - display: 'swap' + subsets: ['latin'], + weight: '500', + variable: '--font-poppins', + display: 'swap' }); -const PostHogPageView = dynamic( - () => import('./integration/posthog/page-view'), - { - ssr: false - } -); +const PostHogPageView = dynamic(() => import('./integration/posthog/page-view'), { + ssr: false +}); // export function generateStaticParams() { // return locales.map((locale: any) => ({ locale })); @@ -75,67 +57,63 @@ const PostHogPageView = dynamic( // } const LocaleLayout = ({ children, params: { locale }, pageProps }: PropsWithChildren) => { - // Validate that the incoming `locale` parameter is valid - if (!locales.includes(locale as string)) notFound(); - const router = useRouter(); - const pathname = usePathname(); - const searchParams = useSearchParams(); - const { isApiWork, loading } = useCheckAPI(); - // Enable static rendering - // unstable_setRequestLocale(locale); - const formatTitle = (url: string) => { - // Separate the URL into pathname and query parts - const [pathname, queryString] = url.split('?'); - - // Ignore language codes or any initial two-letter or specific codes like 'ru', 'ur' - const segments = pathname - .split('/') - .filter((seg) => seg && seg.length > 2) - .map((seg) => { - // Replace dashes with spaces in the segment if it looks like a UUID or has digits (likely an ID) - if (seg.includes('-') || /\d/.test(seg)) { - return ''; // Exclude IDs from title - } - return seg.charAt(0).toUpperCase() + seg.slice(1).toLowerCase(); // Capitalize non-ID segments - }) - .filter((seg: string) => seg); // Remove empty strings resulting from ID exclusion - - // Process query parameters, specifically looking for 'name' - let namePart = ''; - if (queryString) { - const params = new URLSearchParams(queryString); - if (params?.get('name')) { - const name = params.get('name') ?? ''; - const nameValue = - name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); - namePart = nameValue; - } - } - - // Combine the pathname segments with the name part, if present - const title = [...segments, namePart].filter((part) => part).join(' | '); - - return title; - }; - - const name = searchParams?.get('name'); - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const messages = require(`../../locales/${locale}.json`); - - useEffect(() => { - if (!isApiWork && !loading) router.push(`/maintenance`); - else if (isApiWork && pathname?.split('/').reverse()[0] === 'maintenance') - router.replace('/'); - }, [isApiWork, loading, router, pathname]); - return ( - - - - {formatTitle(`${pathname}${name ? `?name=${name}` : ''}`) || 'Home'} - - - {/* + // Validate that the incoming `locale` parameter is valid + if (!locales.includes(locale as string)) notFound(); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const { isApiWork, loading } = useCheckAPI(); + // Enable static rendering + // unstable_setRequestLocale(locale); + const formatTitle = (url: string) => { + // Separate the URL into pathname and query parts + const [pathname, queryString] = url.split('?'); + + // Ignore language codes or any initial two-letter or specific codes like 'ru', 'ur' + const segments = pathname + .split('/') + .filter((seg) => seg && seg.length > 2) + .map((seg) => { + // Replace dashes with spaces in the segment if it looks like a UUID or has digits (likely an ID) + if (seg.includes('-') || /\d/.test(seg)) { + return ''; // Exclude IDs from title + } + return seg.charAt(0).toUpperCase() + seg.slice(1).toLowerCase(); // Capitalize non-ID segments + }) + .filter((seg: string) => seg); // Remove empty strings resulting from ID exclusion + + // Process query parameters, specifically looking for 'name' + let namePart = ''; + if (queryString) { + const params = new URLSearchParams(queryString); + if (params?.get('name')) { + const name = params.get('name') ?? ''; + const nameValue = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); + namePart = nameValue; + } + } + + // Combine the pathname segments with the name part, if present + const title = [...segments, namePart].filter((part) => part).join(' | '); + + return title; + }; + + const name = searchParams?.get('name'); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const messages = require(`../../locales/${locale}.json`); + + useEffect(() => { + if (!isApiWork && !loading) router.push(`/maintenance`); + else if (isApiWork && pathname?.split('/').reverse()[0] === 'maintenance') router.replace('/'); + }, [isApiWork, loading, router, pathname]); + return ( + + + {formatTitle(`${pathname}${name ? `?name=${name}` : ''}`) || 'Home'} + + {/* {GA_MEASUREMENT_ID.value && ( @@ -150,39 +128,35 @@ const LocaleLayout = ({ children, params: { locale }, pageProps }: PropsWithChil )} */} - - - - - - - - - {loading && !pathname?.startsWith('/auth') ? ( - - ) : ( - <> - - {children} - - )} - - - - - - - - ); + + + + + + + + + {loading && !pathname?.startsWith('/auth') ? ( + + ) : ( + <> + + {children} + + )} + + + + + + + + ); }; export default LocaleLayout; diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx index 408cdabb6..febc94842 100644 --- a/apps/web/app/[locale]/page-component.tsx +++ b/apps/web/app/[locale]/page-component.tsx @@ -62,7 +62,7 @@ function MainPage() { } return ( <> -
+
{/*
*/} {/* */} - -
{isTeamMember ? : }
+ + {isTeamMember ? : }
diff --git a/apps/web/app/[locale]/permissions/component.tsx b/apps/web/app/[locale]/permissions/component.tsx index a744d6943..42810dfe6 100644 --- a/apps/web/app/[locale]/permissions/component.tsx +++ b/apps/web/app/[locale]/permissions/component.tsx @@ -1,23 +1,12 @@ 'use client'; -import { - useIsMemberManager, - useOrganizationTeams, - useRolePermissions -} from '@app/hooks'; +import { useIsMemberManager, useOrganizationTeams, useRolePermissions } from '@app/hooks'; import { useRoles } from '@app/hooks/features/useRoles'; import { IRole } from '@app/interfaces'; import { userState } from '@app/stores'; import NotFound from '@components/pages/404'; import { withAuthentication } from 'lib/app/authenticator'; -import { - Breadcrumb, - Card, - CommonToggle, - Container, - Divider, - Text -} from 'lib/components'; +import { Breadcrumb, Card, CommonToggle, Container, Divider, Text } from 'lib/components'; import { MainHeader, MainLayout } from 'lib/layout'; import { useCallback, useEffect, useState } from 'react'; import { useTranslations } from 'next-intl'; @@ -26,314 +15,279 @@ import { useAtom, useAtomValue } from 'jotai'; import { fullWidthState } from '@app/stores/fullWidth'; const Permissions = () => { - const t = useTranslations(); - const { activeTeamManagers } = useOrganizationTeams(); - const { - rolePermissionsFormated, - getRolePermissions, - updateRolePermission - } = useRolePermissions(); + const t = useTranslations(); + const { activeTeamManagers } = useOrganizationTeams(); + const { rolePermissionsFormated, getRolePermissions, updateRolePermission } = useRolePermissions(); - const [selectedRole, setSelectedRole] = useState(null); - const fullWidth = useAtomValue(fullWidthState); + const [selectedRole, setSelectedRole] = useState(null); + const fullWidth = useAtomValue(fullWidthState); - const [user] = useAtom(userState); - const { isTeamManager } = useIsMemberManager(user); + const [user] = useAtom(userState); + const { isTeamManager } = useIsMemberManager(user); - useEffect(() => { - selectedRole && selectedRole?.id && getRolePermissions(selectedRole.id); - }, [selectedRole, getRolePermissions]); + useEffect(() => { + selectedRole && selectedRole?.id && getRolePermissions(selectedRole.id); + }, [selectedRole, getRolePermissions]); - const { getRoles, roles } = useRoles(); - useEffect(() => { - getRoles(); - }, [getRoles]); + const { getRoles, roles } = useRoles(); + useEffect(() => { + getRoles(); + }, [getRoles]); - const handleToggleRolePermission = useCallback( - (name: string) => { - const permission = rolePermissionsFormated[name]; + const handleToggleRolePermission = useCallback( + (name: string) => { + const permission = rolePermissionsFormated[name]; - updateRolePermission({ - ...permission, - enabled: !permission.enabled - }).then(() => { - selectedRole && selectedRole?.id && getRolePermissions(selectedRole.id); - }); - }, - [ - rolePermissionsFormated, - selectedRole, - getRolePermissions, - updateRolePermission - ] - ); + updateRolePermission({ + ...permission, + enabled: !permission.enabled + }).then(() => { + selectedRole && selectedRole?.id && getRolePermissions(selectedRole.id); + }); + }, + [rolePermissionsFormated, selectedRole, getRolePermissions, updateRolePermission] + ); - if (activeTeamManagers && activeTeamManagers.length && !isTeamManager) { - return ( - - - - ); - } + if (activeTeamManagers && activeTeamManagers.length && !isTeamManager) { + return ( + + + + ); + } - return ( - - - - - - -
- {roles.map((role) => ( -
{ - setSelectedRole(role); - }} - > - {role?.name} -
- ))} -
+ return ( + + + + + + +
+ {roles.map((role) => ( +
{ + setSelectedRole(role); + }} + > + {role?.name} +
+ ))} +
- + -
- {selectedRole && ( -
-
- - {t('pages.settingsTeam.TRACK_TIME')} - -
- { - handleToggleRolePermission('TIME_TRACKER'); - }} - /> -
-
-
- - Estimate issue - -
- { - handleToggleRolePermission('ORG_TASK_ADD'); - handleToggleRolePermission('ORG_TASK_EDIT'); - }} - /> -
-
-
- - {t('pages.settingsTeam.EPICS_CREATE_CLOSE')} - -
- { - handleToggleRolePermission('ORG_TASK_ADD'); - handleToggleRolePermission('ORG_TASK_EDIT'); - }} - /> -
-
-
- - {t('pages.settingsTeam.ISSUE_CREATE_CLOSE')} - -
- { - handleToggleRolePermission('ORG_TASK_ADD'); - handleToggleRolePermission('ORG_TASK_EDIT'); - }} - /> -
-
-
- - {t('pages.settingsTeam.ISSUE_ASSIGN_UNASSIGN')} - -
- { - handleToggleRolePermission('ORG_TASK_ADD'); - handleToggleRolePermission('ORG_TASK_EDIT'); - }} - /> -
-
-
- - {t('pages.settingsTeam.INVITE_MEMBERS')} - -
- { - handleToggleRolePermission('ORG_INVITE_EDIT'); - }} - /> -
-
-
- - {t('pages.settingsTeam.REMOVE_MEMBERS')} - -
- { - handleToggleRolePermission('ORG_EMPLOYEES_EDIT'); - handleToggleRolePermission('CHANGE_SELECTED_EMPLOYEE'); - }} - /> -
-
-
- - {t('pages.settingsTeam.HANDLE_REQUESTS')} - -
- { - handleToggleRolePermission( - 'ORG_TEAM_JOIN_REQUEST_EDIT' - ); - }} - /> -
-
-
- - {t('pages.settingsTeam.ROLES_POSITIONS_CHANGE')} - -
- { - handleToggleRolePermission('ORG_EMPLOYEES_EDIT'); - }} - /> -
-
-
- - {t('pages.settingsTeam.VIEW_DETAILS')} - -
- { - handleToggleRolePermission('ORG_TASK_VIEW'); - }} - /> -
-
-
- )} - {!selectedRole && } -
-
-
-
- ); +
+ {selectedRole && ( +
+
+ + {t('pages.settingsTeam.TRACK_TIME')} + +
+ { + handleToggleRolePermission('TIME_TRACKER'); + }} + /> +
+
+
+ + Estimate issue + +
+ { + handleToggleRolePermission('ORG_TASK_ADD'); + handleToggleRolePermission('ORG_TASK_EDIT'); + }} + /> +
+
+
+ + {t('pages.settingsTeam.EPICS_CREATE_CLOSE')} + +
+ { + handleToggleRolePermission('ORG_TASK_ADD'); + handleToggleRolePermission('ORG_TASK_EDIT'); + }} + /> +
+
+
+ + {t('pages.settingsTeam.ISSUE_CREATE_CLOSE')} + +
+ { + handleToggleRolePermission('ORG_TASK_ADD'); + handleToggleRolePermission('ORG_TASK_EDIT'); + }} + /> +
+
+
+ + {t('pages.settingsTeam.ISSUE_ASSIGN_UNASSIGN')} + +
+ { + handleToggleRolePermission('ORG_TASK_ADD'); + handleToggleRolePermission('ORG_TASK_EDIT'); + }} + /> +
+
+
+ + {t('pages.settingsTeam.INVITE_MEMBERS')} + +
+ { + handleToggleRolePermission('ORG_INVITE_EDIT'); + }} + /> +
+
+
+ + {t('pages.settingsTeam.REMOVE_MEMBERS')} + +
+ { + handleToggleRolePermission('ORG_EMPLOYEES_EDIT'); + handleToggleRolePermission('CHANGE_SELECTED_EMPLOYEE'); + }} + /> +
+
+
+ + {t('pages.settingsTeam.HANDLE_REQUESTS')} + +
+ { + handleToggleRolePermission('ORG_TEAM_JOIN_REQUEST_EDIT'); + }} + /> +
+
+
+ + {t('pages.settingsTeam.ROLES_POSITIONS_CHANGE')} + +
+ { + handleToggleRolePermission('ORG_EMPLOYEES_EDIT'); + }} + /> +
+
+
+ + {t('pages.settingsTeam.VIEW_DETAILS')} + +
+ { + handleToggleRolePermission('ORG_TASK_VIEW'); + }} + /> +
+
+
+ )} + {!selectedRole && } +
+
+
+
+ ); }; function SelectRole() { - return ( -
-
- ! -
+ return ( +
+
+ ! +
- - Please Select any Role - -
- ); + + Please Select any Role + +
+ ); } export default withAuthentication(Permissions, { - displayName: 'PermissionPage' + displayName: 'PermissionPage' }); diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index 1eae2807f..c3d41d65c 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -157,7 +157,7 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId
*/} - + {hook.tab == 'worked' && canSeeActivity && (
diff --git a/apps/web/app/[locale]/team/tasks/page.tsx b/apps/web/app/[locale]/team/tasks/page.tsx new file mode 100644 index 000000000..db1844d68 --- /dev/null +++ b/apps/web/app/[locale]/team/tasks/page.tsx @@ -0,0 +1,39 @@ +'use client'; +import { Breadcrumb, Container } from 'lib/components'; +import { MainLayout } from 'lib/layout'; +import { useParams } from 'next/navigation'; +import { useMemo } from 'react'; +import { useTranslations } from 'next-intl'; +import { useAtomValue } from 'jotai'; + +import { fullWidthState } from '@app/stores/fullWidth'; +import { TaskTable } from '@components/pages/team/tasks/TaskTable'; + +import { useOrganizationTeams } from '@app/hooks'; +import { withAuthentication } from '@/lib/app/authenticator'; +const TeamTask = () => { + const t = useTranslations(); + const params = useParams<{ locale: string }>(); + const fullWidth = useAtomValue(fullWidthState); + const currentLocale = params ? params.locale : null; + const { activeTeam } = useOrganizationTeams(); + const breadcrumbPath = useMemo( + () => [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: activeTeam?.name || '', href: '/' }, + { title: "Team's Task", href: `/${currentLocale}/team/task` } + ], + [activeTeam?.name, currentLocale] + ); + + return ( + + + + + + + ); +}; + +export default withAuthentication(TeamTask, { displayName: 'TeamTask' }); diff --git a/apps/web/app/hooks/features/useAuthTeamTasks.ts b/apps/web/app/hooks/features/useAuthTeamTasks.ts index d9734d838..431a57194 100644 --- a/apps/web/app/hooks/features/useAuthTeamTasks.ts +++ b/apps/web/app/hooks/features/useAuthTeamTasks.ts @@ -4,64 +4,60 @@ import { useMemo } from 'react'; import { useAtomValue } from 'jotai'; import { useOrganizationTeams } from './useOrganizationTeams'; import { useDailyPlan } from './useDailyPlan'; -import { - estimatedTotalTime, - getTotalTasks -} from 'lib/features/task/daily-plan'; +import { estimatedTotalTime, getTotalTasks } from 'lib/features/task/daily-plan'; export function useAuthTeamTasks(user: IUser | undefined) { - const tasks = useAtomValue(tasksByTeamState); - const { outstandingPlans, todayPlan, futurePlans } = useDailyPlan(); - - const { activeTeam } = useOrganizationTeams(); - const currentMember = activeTeam?.members?.find( - (member) => member.employee?.userId === user?.id - ); - - const assignedTasks = useMemo(() => { - if (!user) return []; - return tasks.filter((task) => { - return task?.members.some((m) => m.userId === user.id); - }); - }, [tasks, user]); - - const unassignedTasks = useMemo(() => { - if (!user) return []; - return tasks.filter((task) => { - return !task?.members.some((m) => m.userId === user.id); - }); - }, [tasks, user]); - - const planned = useMemo(() => { - const outStandingTasksCount = estimatedTotalTime( - outstandingPlans?.map((plan) => plan.tasks?.map((task) => task)) - ).totalTasks; - - const todayTasksCOunt = getTotalTasks(todayPlan); - - const futureTasksCount = getTotalTasks(futurePlans); - - return outStandingTasksCount + futureTasksCount + todayTasksCOunt; - }, [futurePlans, outstandingPlans, todayPlan]); - - const totalTodayTasks = useMemo( - () => - currentMember?.totalTodayTasks && currentMember?.totalTodayTasks.length - ? currentMember?.totalTodayTasks.map((task) => task.id) - : [], - [currentMember] - ); - - const workedTasks = useMemo(() => { - return tasks.filter((tsk) => { - return totalTodayTasks.includes(tsk.id); - }); - }, [tasks, totalTodayTasks]); - - return { - assignedTasks, - unassignedTasks, - workedTasks, - planned - }; + const tasks = useAtomValue(tasksByTeamState); + const { outstandingPlans, todayPlan, futurePlans } = useDailyPlan(); + + const { activeTeam } = useOrganizationTeams(); + const currentMember = activeTeam?.members?.find((member) => member.employee?.userId === user?.id); + + const assignedTasks = useMemo(() => { + if (!user) return []; + return tasks.filter((task) => { + return task?.members.some((m) => m.userId === user.id); + }); + }, [tasks, user]); + + const unassignedTasks = useMemo(() => { + if (!user) return []; + return tasks.filter((task) => { + return !task?.members.some((m) => m.userId === user.id); + }); + }, [tasks, user]); + + const planned = useMemo(() => { + const outStandingTasksCount = estimatedTotalTime( + outstandingPlans?.map((plan) => plan.tasks?.map((task) => task)) + ).totalTasks; + + const todayTasksCOunt = getTotalTasks(todayPlan); + + const futureTasksCount = getTotalTasks(futurePlans); + + return outStandingTasksCount + futureTasksCount + todayTasksCOunt; + }, [futurePlans, outstandingPlans, todayPlan]); + + const totalTodayTasks = useMemo( + () => + currentMember?.totalTodayTasks && currentMember?.totalTodayTasks.length + ? currentMember?.totalTodayTasks.map((task) => task.id) + : [], + [currentMember] + ); + + const workedTasks = useMemo(() => { + return tasks.filter((tsk) => { + return totalTodayTasks.includes(tsk.id); + }); + }, [tasks, totalTodayTasks]); + + return { + assignedTasks, + unassignedTasks, + workedTasks, + planned, + tasks + }; } diff --git a/apps/web/app/interfaces/ITask.ts b/apps/web/app/interfaces/ITask.ts index adf59e95a..0d12e0718 100644 --- a/apps/web/app/interfaces/ITask.ts +++ b/apps/web/app/interfaces/ITask.ts @@ -34,7 +34,7 @@ export type ITeamTask = { label?: string; parentId?: string; parent?: ITeamTask; - issueType?: string; + issueType?: ITaskIssue; rootEpic?: ITeamTask | null; } & Omit; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 00063f410..d0f7e7050 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -9,9 +9,5 @@ type Props = { // Since we have a `not-found.tsx` page on the root, a layout file // is required, even if it's just passing children through. export default function RootLayout({ children }: Props) { - return ( - - {children} - - ); + return <>{children}; } diff --git a/apps/web/app/stores/menu.ts b/apps/web/app/stores/menu.ts new file mode 100644 index 000000000..415e1f49f --- /dev/null +++ b/apps/web/app/stores/menu.ts @@ -0,0 +1,8 @@ +import { atom } from 'jotai'; +import { atomWithStorage } from 'jotai/utils'; + +export const openMenusState = atom<{ [key: string]: boolean }>({}); + +export const openMenusStateStorage = atomWithStorage<{ [key: string]: boolean }>('sidebar-open-menus', {}); +export const activeMenuIndexState = atom(null); +export const activeSubMenuIndexState = atom(null); diff --git a/apps/web/components/app-sidebar.tsx b/apps/web/components/app-sidebar.tsx index 44536f9a5..eaf3048e7 100644 --- a/apps/web/components/app-sidebar.tsx +++ b/apps/web/components/app-sidebar.tsx @@ -10,7 +10,6 @@ import { X } from 'lucide-react'; -import { TeamItem } from '@/lib/features/team/team-item'; import { EverTeamsLogo, SymbolAppLogo } from '@/lib/components/svgs'; import { NavMain } from '@/components/nav-main'; import { @@ -29,8 +28,6 @@ import Link from 'next/link'; import { cn } from '@/lib/utils'; import { useOrganizationAndTeamManagers } from '@/app/hooks/features/useOrganizationTeamManagers'; import { useAuthenticateUser, useModal, useOrganizationTeams } from '@/app/hooks'; -import { useActiveTeam } from '@/app/hooks/features/useActiveTeam'; -import { SettingOutlineIcon } from '@/assets/svg'; import { useFavoritesTask } from '@/app/hooks/features/useFavoritesTask'; import { Button } from '@/lib/components/button'; import { CreateTeamModal, TaskIssueStatus } from '@/lib/features'; @@ -43,8 +40,7 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) { const { isTeamManager } = useOrganizationTeams(); const { favoriteTasks, toggleFavorite } = useFavoritesTask(); const { state } = useSidebar(); - const { onChangeActiveTeam, activeTeam } = useActiveTeam(); - const { isOpen, closeModal, openModal } = useModal(); + const { isOpen, closeModal } = useModal(); const t = useTranslations(); // This is sample data. const data = { @@ -69,60 +65,60 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) { items: favoriteTasks && favoriteTasks.length > 0 ? favoriteTasks - .sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())) - .map((task, index) => ({ - title: task?.title, - url: '#', - component: ( - - - - {task && ( - // Show task issue and task number - - )} - - - #{task?.taskNumber} - - - {task?.title} + .sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())) + .map((task, index) => ({ + title: task?.title, + url: '#', + component: ( + + + + {task && ( + // Show task issue and task number + + )} + + + #{task?.taskNumber} + + + {task?.title} + - - - toggleFavorite(task)} - /> - - - ) - })) + + toggleFavorite(task)} + /> + + + ) + })) : [ - { - title: t('common.NO_FAVORITE_TASK'), - url: '#', - label: 'no-task' - } - ] + { + title: t('common.NO_FAVORITE_TASK'), + url: '#', + label: 'no-task' + } + ] }, { title: t('sidebar.TASKS'), @@ -132,137 +128,109 @@ export function AppSidebar({ publicTeam, ...props }: AppSidebarProps) { items: [ { title: t('sidebar.TEAMTASKS'), - url: '/' + url: '/team/tasks', + label: 'team-tasks' }, { title: t('sidebar.MY_TASKS'), - url: `/profile/${user?.id}?name=${username || ''}` + url: `/profile/${user?.id}?name=${encodeURIComponent(username || '')}`, + label: 'my-tasks' } ] }, ...(userManagedTeams && userManagedTeams.length > 0 ? [ - { - title: t('sidebar.PROJECTS'), - label: 'projects', - url: '#', - icon: FolderKanban, - items: [ - ...userManagedTeams - .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) - .map((team, index) => ({ - title: team.name, + { + title: t('sidebar.PROJECTS'), + label: 'projects', + url: '#', + icon: FolderKanban, + items: [ + { + title: t('common.NO_PROJECT'), + label: 'no-project', url: '#', component: ( - - + + {t('common.CREATE_PROJECT')} + ) - })), - { - title: t('common.NO_PROJECT'), - url: '#', - component: ( - - - - ) - } - ] - } - ] + } + ] + } + ] : []), { title: t('sidebar.MY_WORKS'), url: '#', icon: MonitorSmartphone, + label: 'my-work', items: [ { title: t('sidebar.TIME_AND_ACTIVITY'), + label: 'time-and-activity', url: '#' }, { title: t('sidebar.WORK_DIARY'), + label: 'work-and-diary', url: '#' } ] }, ...(isTeamManager ? [ - { - title: t('sidebar.REPORTS'), - url: '#', - icon: SquareActivity, - items: [ - { - title: t('sidebar.TIMESHEETS'), - url: `/timesheet/${user?.id}?name=${username || ''}` - }, - { - title: t('sidebar.MANUAL_TIME_EDIT'), - url: '#' - }, - { - title: t('sidebar.WEEKLY_LIMIT'), - url: '#' - }, - { - title: t('sidebar.ACTUAL_AND_EXPECTED_HOURS'), - url: '#' - }, - { - title: t('sidebar.PAYMENTS_DUE'), - url: '#' - }, - { - title: t('sidebar.PROJECT_BUDGET'), - url: '#' - }, - { - title: t('sidebar.TIME_AND_ACTIVITY'), - url: '#' - } - ] - } - ] + { + title: t('sidebar.REPORTS'), + label: 'reports', + url: '#', + icon: SquareActivity, + items: [ + { + title: t('sidebar.TIMESHEETS'), + url: `/timesheet/${user?.id}?name=${encodeURIComponent(username || '')}`, + label: 'timesheets' + }, + { + title: t('sidebar.MANUAL_TIME_EDIT'), + label: 'manual-time-edit', + url: '#' + }, + { + title: t('sidebar.WEEKLY_LIMIT'), + label: 'weekly-limit', + url: '#' + }, + { + title: t('sidebar.ACTUAL_AND_EXPECTED_HOURS'), + label: 'actual-and-expected-hours', + url: '#' + }, + { + title: t('sidebar.PAYMENTS_DUE'), + label: 'payments-due', + url: '#' + }, + { + title: t('sidebar.PROJECT_BUDGET'), + label: 'project-budget', + url: '#' + }, + { + title: t('sidebar.TIME_AND_ACTIVITY'), + label: 'time-and-activity', + url: '#' + } + ] + } + ] : []) ] }; diff --git a/apps/web/components/nav-main.tsx b/apps/web/components/nav-main.tsx index 207f42fa0..8dfd21d4b 100644 --- a/apps/web/components/nav-main.tsx +++ b/apps/web/components/nav-main.tsx @@ -1,5 +1,5 @@ 'use client'; - +import { useAtom } from 'jotai'; import { ChevronRight, type LucideIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; @@ -12,9 +12,11 @@ import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, + SidebarTriggerButton, useSidebar } from '@/components/ui/sidebar'; import Link from 'next/link'; +import { activeMenuIndexState, activeSubMenuIndexState, openMenusState } from '@/app/stores/menu'; export function NavMain({ items @@ -32,32 +34,107 @@ export function NavMain({ }[]; }>) { const { state } = useSidebar(); + const [openMenus, setOpenMenus] = useAtom(openMenusState); + const [activeMenuIndex, setActiveMenuIndex] = useAtom(activeMenuIndexState); + const [activeSubMenuIndex, setActiveSubMenuIndex] = useAtom(activeSubMenuIndexState); + + const handleMenuToggle = (label: string, index: number) => { + setOpenMenus((prev) => ({ + ...prev, + [label]: !prev[label] // Reverses the opening state of the selected menu + })); + + // Close all other sub-menus + setOpenMenus((prev) => { + const newState = { ...prev }; + Object.keys(prev).forEach((key) => { + if (key !== label) { + newState[key] = false; + } + }); + return newState; + }); + + setActiveMenuIndex(index); + setActiveSubMenuIndex(null); // Reset active sub-menu index + }; + + const handleSubMenuToggle = (subIndex: number) => { + setActiveSubMenuIndex(subIndex); + }; return ( - {items.map((item) => ( - + {items.map((item, index) => ( + handleMenuToggle(item.title, index)} + > - - - - + - {item.title} - - - + + {state === 'collapsed' ? ( + + + + ) : ( + + )} + + + {item.title} + + + + + ) : ( + + + {state === 'collapsed' ? ( + + + + ) : ( + + )} + + {item.title} + + + + )} + {item.items?.length ? ( <> @@ -72,7 +149,15 @@ export function NavMain({ {subItem?.component || ( handleSubMenuToggle(key)} asChild > diff --git a/apps/web/components/pages/task/description-block/task-description-editor.tsx b/apps/web/components/pages/task/description-block/task-description-editor.tsx index e059f444d..4e7dd589d 100644 --- a/apps/web/components/pages/task/description-block/task-description-editor.tsx +++ b/apps/web/components/pages/task/description-block/task-description-editor.tsx @@ -1,19 +1,8 @@ import Toolbar from './editor-toolbar'; -import { - TextEditorService, - withHtml, - withChecklists, - isValidSlateObject -} from './editor-components/TextEditorService'; +import { TextEditorService, withHtml, withChecklists, isValidSlateObject } from './editor-components/TextEditorService'; import isHotkey from 'is-hotkey'; import { useCallback, useMemo, useRef, useState } from 'react'; -import { - Editor, - createEditor, - Element as SlateElement, - Descendant, - Transforms -} from 'slate'; +import { Editor, createEditor, Element as SlateElement, Descendant, Transforms } from 'slate'; import { withHistory } from 'slate-history'; import { Editable, withReact, Slate } from 'slate-react'; import EditorFooter from './editor-footer'; @@ -26,252 +15,247 @@ import { configHtmlToSlate } from './editor-components/serializerConfigurations' import CheckListElement from './editor-components/CheckListElement'; const HOTKEYS: { [key: string]: string } = { - 'mod+b': 'bold', - 'mod+i': 'italic', - 'mod+u': 'underline', - 'mod+`': 'code' + 'mod+b': 'bold', + 'mod+i': 'italic', + 'mod+u': 'underline', + 'mod+`': 'code' }; interface IRichTextProps { - defaultValue?: string; - readonly?: boolean; - handleTemplateChange?: (key: string, value: any) => void; + defaultValue?: string; + readonly?: boolean; + handleTemplateChange?: (key: string, value: any) => void; } const RichTextEditor = ({ readonly }: IRichTextProps) => { - const renderElement = useCallback((props: any) => , []); - const renderLeaf = useCallback((props: any) => , []); - const editor = useMemo( - () => withChecklists(withHtml(withHistory(withReact(createEditor())))), - [] - ); - const [task] = useAtom(detailedTaskState); - const [isUpdated, setIsUpdated] = useState(false); - const [editorValue, setEditorValue] = useState(); - const editorRef = useRef(null); + const renderElement = useCallback((props: any) => , []); + const renderLeaf = useCallback((props: any) => , []); + const editor = useMemo(() => withChecklists(withHtml(withHistory(withReact(createEditor())))), []); + const [task] = useAtom(detailedTaskState); + const [isUpdated, setIsUpdated] = useState(false); + const [editorValue, setEditorValue] = useState(); + const editorRef = useRef(null); - const initialValue = useMemo((): Descendant[] => { - let value; - if (task && task.description) { - if (isHtml(task.description)) { - // when value is an HTML - value = htmlToSlate(task.description, configHtmlToSlate); - } else if (isValidSlateObject(task.description)) { - //when value is Slate Object - value = JSON.parse(task.description) as Descendant[]; - } else { - // Default case when the task.description is plain text - value = [ - { - //@ts-ignore - type: 'paragraph', - children: [{ text: task.description as string }] - } - ]; - } - } else { - value = [{ type: 'paragraph', children: [{ text: '' }] }]; - } - setEditorValue(value); - return value; - }, [task]); + const initialValue = useMemo((): Descendant[] => { + let value; + if (task && task.description) { + if (isHtml(task.description)) { + // when value is an HTML + value = htmlToSlate(task.description, configHtmlToSlate); + } else if (isValidSlateObject(task.description)) { + //when value is Slate Object + value = JSON.parse(task.description) as Descendant[]; + } else { + // Default case when the task.description is plain text + value = [ + { + //@ts-ignore + type: 'paragraph', + children: [{ text: task.description as string }] + } + ]; + } + } else { + value = [{ type: 'paragraph', children: [{ text: '' }] }]; + } + setEditorValue(value); + return value; + }, [task]); - const clearUnsavedValues = () => { - // Delete all entries leaving 1 empty node - Transforms.delete(editor, { - at: { - anchor: Editor.start(editor, []), - focus: Editor.end(editor, []) - } - }); + const clearUnsavedValues = () => { + // Delete all entries leaving 1 empty node + Transforms.delete(editor, { + at: { + anchor: Editor.start(editor, []), + focus: Editor.end(editor, []) + } + }); - // Removes empty node - Transforms.removeNodes(editor, { - at: [0] - }); + // Removes empty node + Transforms.removeNodes(editor, { + at: [0] + }); - // Insert array of children nodes - Transforms.insertNodes(editor, initialValue); + // Insert array of children nodes + Transforms.insertNodes(editor, initialValue); - setIsUpdated(false); - }; + setIsUpdated(false); + }; - const selectEmoji = (emoji: { native: string }) => { - const { selection } = editor; - if (selection) { - const [start] = Editor.edges(editor, selection); - Transforms.insertText(editor, emoji.native, { at: start }); - Transforms.collapse(editor, { edge: 'end' }); - } - setIsUpdated(false); - }; + const selectEmoji = (emoji: { native: string }) => { + const { selection } = editor; + if (selection) { + const [start] = Editor.edges(editor, selection); + Transforms.insertText(editor, emoji.native, { at: start }); + Transforms.collapse(editor, { edge: 'end' }); + } + setIsUpdated(false); + }; - return ( -
- {task && ( - { - setEditorValue(e); - setIsUpdated(true); - }} - > - -
+ return ( +
+ {task && ( + { + setEditorValue(e); + setIsUpdated(true); + }} + > + +
- ( -
-
{children}
-
- )} - renderElement={renderElement} - renderLeaf={renderLeaf} - placeholder="Write a complete description of your project..." - spellCheck - readOnly={readonly} - onKeyDown={(event) => { - for (const hotkey in HOTKEYS) { - if (isHotkey(hotkey, event as any)) { - event.preventDefault(); - const mark = HOTKEYS[hotkey]; - TextEditorService.toggleMark(editor, mark, isMarkActive); - } - } - }} - /> - setIsUpdated(false)} - editorValue={editorValue} - editorRef={editorRef} - clearUnsavedValues={clearUnsavedValues} - /> -
- )} -
- ); + ( +
+
{children}
+
+ )} + renderElement={renderElement} + renderLeaf={renderLeaf} + placeholder="Write a complete description of your project..." + spellCheck + readOnly={readonly} + onKeyDown={(event) => { + for (const hotkey in HOTKEYS) { + if (isHotkey(hotkey, event as any)) { + event.preventDefault(); + const mark = HOTKEYS[hotkey]; + TextEditorService.toggleMark(editor, mark, isMarkActive); + } + } + }} + /> + setIsUpdated(false)} + editorValue={editorValue} + editorRef={editorRef} + clearUnsavedValues={clearUnsavedValues} + /> +
+ )} +
+ ); }; const isBlockActive = (editor: any, format: string, blockType = 'type') => { - const { selection } = editor; - if (!selection) return false; + const { selection } = editor; + if (!selection) return false; - const [match] = Array.from( - Editor.nodes(editor, { - at: Editor.unhangRange(editor, selection), - match: (n) => - !Editor.isEditor(n) && - SlateElement.isElement(n) && - (n as { [key: string]: any })[blockType] === format - }) - ); + const [match] = Array.from( + Editor.nodes(editor, { + at: Editor.unhangRange(editor, selection), + match: (n) => + !Editor.isEditor(n) && SlateElement.isElement(n) && (n as { [key: string]: any })[blockType] === format + }) + ); - return !!match; + return !!match; }; const isMarkActive = (editor: any, format: string) => { - const marks = Editor.marks(editor); - return marks ? (marks as { [key: string]: any })[format] === true : false; + const marks = Editor.marks(editor); + return marks ? (marks as { [key: string]: any })[format] === true : false; }; const Element = ({ attributes, children, element }: any) => { - const style = { textAlign: element.align }; - switch (element.type) { - case 'blockquote': - return ( -
- {children} -
- ); - case 'code': - return ( -
-          {children}
-        
- ); - case 'ul': - return ( -
    - {children} -
- ); - case 'h1': - return ( -

- {children} -

- ); - case 'h2': - return ( -

- {children} -

- ); - case 'li': // Render
  • as a block element - return ( -
  • - {children} -
  • - ); - case 'ol': - return ( -
      - {children} -
    - ); - case 'link': - return ( - - {children} - - ); - case 'checklist': - return ( - - {children} - - ); - default: - return ( -

    - {children} -

    - ); - } + const style = { textAlign: element.align }; + switch (element.type) { + case 'blockquote': + return ( +
    + {children} +
    + ); + case 'code': + return ( +
    +					{children}
    +				
    + ); + case 'ul': + return ( +
      + {children} +
    + ); + case 'h1': + return ( +

    + {children} +

    + ); + case 'h2': + return ( +

    + {children} +

    + ); + case 'li': // Render
  • as a block element + return ( +
  • + {children} +
  • + ); + case 'ol': + return ( +
      + {children} +
    + ); + case 'link': + return ( + + {children} + + ); + case 'checklist': + return ( + + {children} + + ); + default: + return ( +

    + {children} +

    + ); + } }; const Leaf = ({ attributes, children, leaf }: any) => { - if (leaf.bold) { - children = {children}; - } + if (leaf.bold) { + children = {children}; + } - if (leaf.code) { - children = {children}; - } + if (leaf.code) { + children = {children}; + } - if (leaf.italic) { - children = {children}; - } + if (leaf.italic) { + children = {children}; + } - if (leaf.underline) { - children = {children}; - } + if (leaf.underline) { + children = {children}; + } - return {children}; + return {children}; }; export default RichTextEditor; diff --git a/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx index ce6bcc4e1..60f2a0d8d 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx @@ -1,25 +1,13 @@ /* eslint-disable no-mixed-spaces-and-tabs */ import { calculateRemainingDays, formatDateString } from '@app/helpers'; -import { - useOrganizationTeams, - useSyncRef, - useTeamMemberCard, - useTeamTasks -} from '@app/hooks'; +import { useOrganizationTeams, useSyncRef, useTeamMemberCard, useTeamTasks } from '@app/hooks'; import { ITeamTask, OT_Member } from '@app/interfaces'; import { detailedTaskState } from '@app/stores'; import { clsxm } from '@app/utils'; import { Popover, Transition } from '@headlessui/react'; import { TrashIcon } from 'assets/svg'; import { ActiveTaskIssuesDropdown } from 'lib/features'; -import { - Fragment, - forwardRef, - useCallback, - useEffect, - useMemo, - useState -} from 'react'; +import { Fragment, forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import { useAtom } from 'jotai'; import ProfileInfo from '../components/profile-info'; import TaskRow from '../components/task-row'; @@ -30,377 +18,327 @@ import { useTranslations } from 'next-intl'; import { PencilSquareIcon } from '@heroicons/react/20/solid'; const TaskMainInfo = () => { - const [task] = useAtom(detailedTaskState); - const { activeTeam } = useOrganizationTeams(); - const t = useTranslations(); + const [task] = useAtom(detailedTaskState); + const { activeTeam } = useOrganizationTeams(); + const t = useTranslations(); - return ( -
    - - - - - {task?.creator && ( - - - - )} - - -
    - {task?.members?.map((member: any) => ( - - - - ))} + return ( +
    + + + + + {task?.creator && ( + + + + )} + + +
    + {task?.members?.map((member: any) => ( + + + + ))} - {ManageMembersPopover(activeTeam?.members || [], task)} -
    -
    + {ManageMembersPopover(activeTeam?.members || [], task)} +
    +
    - -
    - ); + + + ); }; -const DateCustomInput = forwardRef>( - (props, ref) => { - return
    ; - } -); +const DateCustomInput = forwardRef>((props, ref) => { + return
    ; +}); DateCustomInput.displayName = 'DateCustomInput'; function DueDates() { - const { updateTask } = useTeamTasks(); - const [task] = useAtom(detailedTaskState); - const t = useTranslations(); - const [startDate, setStartDate] = useState(null); - const [dueDate, setDueDate] = useState(null); + const { updateTask } = useTeamTasks(); + const [task] = useAtom(detailedTaskState); + const t = useTranslations(); + const [startDate, setStartDate] = useState(null); + const [dueDate, setDueDate] = useState(null); - const $startDate = useSyncRef( - startDate || (task?.startDate ? new Date(task.startDate) : null) - ); + const $startDate = useSyncRef(startDate || (task?.startDate ? new Date(task.startDate) : null)); - const $dueDate = useSyncRef( - dueDate || (task?.dueDate ? new Date(task.dueDate) : null) - ); + const $dueDate = useSyncRef(dueDate || (task?.dueDate ? new Date(task.dueDate) : null)); - const remainingDays = task - ? calculateRemainingDays(new Date().toISOString(), task.dueDate) - : undefined; + const remainingDays = task ? calculateRemainingDays(new Date().toISOString(), task.dueDate) : undefined; - const handleResetDate = useCallback( - (date: 'startDate' | 'dueDate') => { - if (date === 'startDate') { - setStartDate(null); - $startDate.current = null; - } - if (date === 'dueDate') { - setDueDate(null); - $dueDate.current = null; - } + const handleResetDate = useCallback( + (date: 'startDate' | 'dueDate') => { + if (date === 'startDate') { + setStartDate(null); + $startDate.current = null; + } + if (date === 'dueDate') { + setDueDate(null); + $dueDate.current = null; + } - if (task) { - updateTask({ ...task, [date]: null }); - } - }, - [$startDate, $dueDate, task, updateTask] - ); + if (task) { + updateTask({ ...task, [date]: null }); + } + }, + [$startDate, $dueDate, task, updateTask] + ); - return ( -
    - - - {startDate ? ( - formatDateString(startDate.toISOString()) - ) : task?.startDate ? ( - formatDateString(task?.startDate) - ) : ( - - )} -
    - } - selected={ - $startDate.current - ? (new Date($startDate.current) as Date) - : undefined - } - onSelect={(date) => { - if (date && (!$dueDate.current || date <= $dueDate.current)) { - setStartDate(date); + return ( +
    + + + {startDate ? ( + formatDateString(startDate.toISOString()) + ) : task?.startDate ? ( + formatDateString(task?.startDate) + ) : ( + + )} +
    + } + selected={$startDate.current ? (new Date($startDate.current) as Date) : undefined} + onSelect={(date) => { + if (date && (!$dueDate.current || date <= $dueDate.current)) { + setStartDate(date); - if (task) { - updateTask({ ...task, startDate: date?.toISOString() }); - } - } - }} - mode={'single'} - /> - {task?.startDate ? ( - { - handleResetDate('startDate'); - }} - > - - - ) : ( - <> - )} - + if (task) { + updateTask({ ...task, startDate: date?.toISOString() }); + } + } + }} + mode={'single'} + /> + {task?.startDate ? ( + { + handleResetDate('startDate'); + }} + > + + + ) : ( + <> + )} + - - - {dueDate ? ( - formatDateString(dueDate.toISOString()) - ) : task?.dueDate ? ( - formatDateString(task?.dueDate) - ) : ( - - )} -
    - } - selected={ - $dueDate.current ? (new Date($dueDate.current) as Date) : undefined - } - onSelect={(date) => { - if ( - (!$startDate.current && date) || - ($startDate.current && date && date >= $startDate.current) - ) { - setDueDate(date); - if (task) { - updateTask({ ...task, dueDate: date?.toISOString() }); - } - } - }} - mode={'single'} - /> - {task?.dueDate ? ( - { - handleResetDate('dueDate'); - }} - > - - - ) : ( - <> - )} - + + + {dueDate ? ( + formatDateString(dueDate.toISOString()) + ) : task?.dueDate ? ( + formatDateString(task?.dueDate) + ) : ( + + )} +
    + } + selected={$dueDate.current ? (new Date($dueDate.current) as Date) : undefined} + onSelect={(date) => { + if ( + (!$startDate.current && date) || + ($startDate.current && date && date >= $startDate.current) + ) { + setDueDate(date); + if (task) { + updateTask({ ...task, dueDate: date?.toISOString() }); + } + } + }} + mode={'single'} + /> + {task?.dueDate ? ( + { + handleResetDate('dueDate'); + }} + > + + + ) : ( + <> + )} + - {task?.dueDate && ( - -
    - {remainingDays !== undefined && remainingDays < 0 - ? 0 - : remainingDays} -
    -
    - )} -
    - ); + {task?.dueDate && ( + +
    + {remainingDays !== undefined && remainingDays < 0 ? 0 : remainingDays} +
    +
    + )} +
    + ); } -const ManageMembersPopover = ( - memberList: OT_Member[], - task: ITeamTask | null -) => { - const t = useTranslations(); - const [member, setMember] = useState(); - const [memberToRemove, setMemberToRemove] = useState(false); - const [memberToAdd, setMemberToAdd] = useState(false); +const ManageMembersPopover = (memberList: OT_Member[], task: ITeamTask | null) => { + const t = useTranslations(); + const [member, setMember] = useState(); + const [memberToRemove, setMemberToRemove] = useState(false); + const [memberToAdd, setMemberToAdd] = useState(false); - const memberInfo = useTeamMemberCard(member); + const memberInfo = useTeamMemberCard(member); - const unassignedMembers = useMemo( - () => - memberList.filter((member) => - member.employee - ? !task?.members - .map((item) => item.userId) - .includes(member.employee.userId) && member.employee?.isActive - : false - ), - [memberList, task?.members] - ); + const unassignedMembers = useMemo( + () => + memberList.filter((member) => + member.employee + ? !task?.members.map((item) => item.userId).includes(member.employee.userId) && + member.employee?.isActive + : false + ), + [memberList, task?.members] + ); - const assignedTaskMembers = useMemo( - () => - memberList.filter((member) => - member.employee - ? task?.members - .map((item) => item.userId) - .includes(member.employee?.userId) && member.employee?.isActive - : false - ), - [memberList, task?.members] - ); + const assignedTaskMembers = useMemo( + () => + memberList.filter((member) => + member.employee + ? task?.members.map((item) => item.userId).includes(member.employee?.userId) && + member.employee?.isActive + : false + ), + [memberList, task?.members] + ); - useEffect(() => { - if (task && member && memberToRemove) { - memberInfo - .unassignTask(task) - .then(() => { - setMember(undefined); - setMemberToRemove(false); - }) - .catch(() => { - setMember(undefined); - setMemberToRemove(false); - }); - } else if (task && member && memberToAdd) { - memberInfo - .assignTask(task) - .then(() => { - setMember(undefined); - setMemberToAdd(false); - }) - .catch(() => { - setMember(undefined); - setMemberToAdd(false); - }); - } - }, [task, member, memberInfo, memberToAdd, memberToRemove]); + useEffect(() => { + if (task && member && memberToRemove) { + memberInfo + .unassignTask(task) + .then(() => { + setMember(undefined); + setMemberToRemove(false); + }) + .catch(() => { + setMember(undefined); + setMemberToRemove(false); + }); + } else if (task && member && memberToAdd) { + memberInfo + .assignTask(task) + .then(() => { + setMember(undefined); + setMemberToAdd(false); + }) + .catch(() => { + setMember(undefined); + setMemberToAdd(false); + }); + } + }, [task, member, memberInfo, memberToAdd, memberToRemove]); - return ( - <> - {task && memberList.length > 1 ? ( - - - - {({ close }) => ( -
    - {assignedTaskMembers.map((member, index) => ( -
    { - setMember(member); - setMemberToRemove(true); - close(); - }} - key={index} - > - + return ( + <> + {task && memberList.length > 1 ? ( + + + + {({ close }) => ( +
    + {assignedTaskMembers.map((member, index) => ( +
    { + setMember(member); + setMemberToRemove(true); + close(); + }} + key={index} + > + - -
    - ))} - {unassignedMembers.map((member, index) => ( -
    { - setMember(member); - setMemberToAdd(true); - close(); - }} - key={index} - > - -
    - ))} -
    - )} -
    -
    + +
    + ))} + {unassignedMembers.map((member, index) => ( +
    { + setMember(member); + setMemberToAdd(true); + close(); + }} + key={index} + > + +
    + ))} +
    + )} +
    +
    - -
    -

    - {t('pages.settingsTeam.MANAGE_ASSIGNEES')} -

    -
    -
    -
    - ) : ( - <> - )} - - ); + +
    +

    + {t('pages.settingsTeam.MANAGE_ASSIGNEES')} +

    +
    +
    + + ) : ( + <> + )} + + ); }; export default TaskMainInfo; diff --git a/apps/web/components/pages/team/tasks/AssigneeUser.tsx b/apps/web/components/pages/team/tasks/AssigneeUser.tsx new file mode 100644 index 000000000..b300c8d9b --- /dev/null +++ b/apps/web/components/pages/team/tasks/AssigneeUser.tsx @@ -0,0 +1,30 @@ +import { IEmployee } from '@/app/interfaces'; +import { Avatar, AvatarFallback, AvatarImage } from '@components/ui/avatar'; +import { FC } from 'react'; + +const AssigneeUser: FC<{ users: IEmployee[] }> = ({ users }) => { + const employee = users && users.length > 0 ? users.at(0) : null; + return ( +
    + {employee ? ( +
    + + {employee?.user?.imageUrl && ( + + )} + + + + + {employee.fullName} +
    + ) : ( + No user assigned + )} +
    + ); +}; + +export default AssigneeUser; diff --git a/apps/web/components/pages/team/tasks/DropdownMenuTask.tsx b/apps/web/components/pages/team/tasks/DropdownMenuTask.tsx new file mode 100644 index 000000000..2e8f6ce08 --- /dev/null +++ b/apps/web/components/pages/team/tasks/DropdownMenuTask.tsx @@ -0,0 +1,92 @@ +import { Button } from '@components/ui/button'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator +} from '@components/ui/dropdown-menu'; +import { useAuthenticateUser, useOrganizationTeams, useTeamMemberCard, useTMCardTaskEdit } from '@app/hooks'; +import { useTranslations } from 'next-intl'; +import { useFavoritesTask } from '@/app/hooks/features/useFavoritesTask'; +import { ITeamTask } from '@app/interfaces'; +import { FC, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; + +const DropdownMenuTask: FC<{ task: ITeamTask }> = ({ task }) => { + const { activeTeam } = useOrganizationTeams(); + const router = useRouter(); + const { user } = useAuthenticateUser(); + const member = activeTeam?.members.find((m) => m?.employee?.user?.id === user?.id); + const memberInfo = useTeamMemberCard(member); + const taskEdition = useTMCardTaskEdit(task); + + const { toggleFavorite, isFavorite } = useFavoritesTask(); + const t = useTranslations(); + + const handleAssignment = useCallback(() => { + if (memberInfo.member?.employee?.user?.id === user?.id) { + memberInfo.unassignTask(task); + } else { + memberInfo.assignTask(task); + } + }, [memberInfo, task]); + + return ( + + + + + + taskEdition?.task?.id && navigator.clipboard.writeText(taskEdition.task.id)} + > + Copy Task ID + + + + router.push(`/task/${task.id}`)}> + {t('common.TASK_DETAILS')} + + + toggleFavorite(task)}> + {isFavorite(task) ? t('common.REMOVE_FAVORITE_TASK') : t('common.ADD_FAVORITE_TASK')} + + + + {memberInfo.member?.employee?.user?.id !== user?.id + ? t('common.ASSIGN_TASK') + : t('common.UNASSIGN_TASK')} + + + + ); +}; + +export default DropdownMenuTask; diff --git a/apps/web/components/pages/team/tasks/FilterButton.tsx b/apps/web/components/pages/team/tasks/FilterButton.tsx new file mode 100644 index 000000000..d7524eeea --- /dev/null +++ b/apps/web/components/pages/team/tasks/FilterButton.tsx @@ -0,0 +1,97 @@ +import { Button } from '@/components/ui/button'; +import { Table } from '@tanstack/react-table'; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger +} from '@components/ui/dropdown-menu'; + +interface FilterButtonProps { + table: Table; +} + +export default function FilterButton({ table }: Readonly>) { + return ( + + + + + + {table.getAllColumns().map((column) => { + if (column.getCanFilter()) { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ); + } + })} + + + ); +} diff --git a/apps/web/components/pages/team/tasks/StatusBadge.tsx b/apps/web/components/pages/team/tasks/StatusBadge.tsx new file mode 100644 index 000000000..869d9bcba --- /dev/null +++ b/apps/web/components/pages/team/tasks/StatusBadge.tsx @@ -0,0 +1,23 @@ +import { cn } from '@/lib/utils'; +import React from 'react'; + +interface StatusBadgeProps { + label: string; + count: number; + color: string; +} + +const StatusBadge: React.FC = ({ label, count, color }) => { + return ( +
    + {label} ({count}) +
    + ); +}; + +export default StatusBadge; diff --git a/apps/web/components/pages/team/tasks/TaskTable.tsx b/apps/web/components/pages/team/tasks/TaskTable.tsx new file mode 100644 index 000000000..3bcd0f34e --- /dev/null +++ b/apps/web/components/pages/team/tasks/TaskTable.tsx @@ -0,0 +1,30 @@ +'use client'; +import { usePagination } from '@/app/hooks/features/usePagination'; +import { columns } from './columns'; +import { TasksDataTable } from './tasks-data-table'; +import { useTeamTasks } from '@/app/hooks'; +import { Paginate } from '@/lib/components'; +import { ITeamTask } from '@/app/interfaces'; + +export function TaskTable() { + const { tasks } = useTeamTasks(); + + const { total, onPageChange, itemsPerPage, itemOffset, endOffset, setItemsPerPage, currentItems } = + usePagination(tasks); + return ( +
    + + + +
    + ); +} diff --git a/apps/web/components/pages/team/tasks/columns.tsx b/apps/web/components/pages/team/tasks/columns.tsx new file mode 100644 index 000000000..5225c2a45 --- /dev/null +++ b/apps/web/components/pages/team/tasks/columns.tsx @@ -0,0 +1,135 @@ +import { ITeamTask } from '@/app/interfaces'; +import { ColumnDef } from '@tanstack/react-table'; +import { Bug } from 'lucide-react'; +import AssigneeUser from './AssigneeUser'; +import { ActiveTaskStatusDropdown } from '@/lib/features'; +import DropdownMenuTask from './DropdownMenuTask'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'typeNumber', + header: 'Type + Number', + cell: ({ row }) => ( +
    + {row.original.issueType ? ( + <> + + {row.original.issueType === 'Bug' ? ( + + ) : row.original.issueType === 'Story' ? ( + + + + + ) : ( + + + + + + + + )} + + + + #{row.original.number} + + + ) : ( + No issue Type + )} +
    + ) + }, + { + accessorKey: 'title', + header: 'Issue Details', + cell: ({ row }) => ( +
    {row.original.title}
    + ), + enableColumnFilter: true + }, + { + accessorKey: 'user', + header: 'Assignee', + cell: ({ row }) => + }, + { + accessorKey: 'status', + header: 'Status', + + cell: ({ row }) => ( + + ) + }, + { + id: 'actions', + header: 'Action', + cell: ({ row }) => { + return ; + } + } +]; diff --git a/apps/web/components/pages/team/tasks/tasks-data-table.tsx b/apps/web/components/pages/team/tasks/tasks-data-table.tsx new file mode 100644 index 000000000..d8dd86767 --- /dev/null +++ b/apps/web/components/pages/team/tasks/tasks-data-table.tsx @@ -0,0 +1,170 @@ +import { ColumnDef, flexRender, getCoreRowModel, getFilteredRowModel, useReactTable } from '@tanstack/react-table'; +import { cn } from '@/lib/utils'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@components/ui/table'; +import { Button } from '@/components/ui/button'; +import FilterButton from './FilterButton'; +import StatusBadge from './StatusBadge'; +import { ITaskStatus, ITeamTask } from '@/app/interfaces'; +import { Input } from '@components/ui/input'; +import { Search } from 'lucide-react'; +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + className?: string; +} + +export function TasksDataTable({ columns, data, className }: Readonly>) { + const table = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel() + }); + const tasks = data as ITeamTask[]; + return ( + <> +
    +
    +

    + Team Tasks +

    + +
    +
    + +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + {table.getFilteredRowModel().rows.length ? ( + + {table.getFilteredRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + ) : ( +
    + + + + + + +

    No data to show

    +
    + )} +
    +
    + + ); +} + +function getStatusColor(status: ITaskStatus) { + switch (status) { + case 'in-review': + return 'bg-[#f3d8b0]'; + case 'backlog': + return 'bg-[#ffcc00]'; + case 'open': + return 'bg-[#d6e4f9]'; + case 'in-progress': + return 'bg-[#ece8fc]'; + case 'ready-for-review': + return 'bg-[#f5f1cb]'; + case 'blocked': + return 'bg-[#f5b8b8]'; + case 'done': + return 'bg-[#4caf50] text-gray-100'; + case 'completed': + return 'bg-[#d4efdf]'; + case 'custom': + return 'bg-[#d4efdf]'; + default: + return 'bg-gray-100 text-gray-800'; + } +} diff --git a/apps/web/components/ui/sidebar.tsx b/apps/web/components/ui/sidebar.tsx index a373c39f0..092b081de 100644 --- a/apps/web/components/ui/sidebar.tsx +++ b/apps/web/components/ui/sidebar.tsx @@ -256,8 +256,29 @@ const SidebarTrigger = React.forwardRef, React.C ); } ); -SidebarTrigger.displayName = 'SidebarTrigger'; +const SidebarTriggerButton = React.forwardRef, React.ComponentProps>( + ({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + ); + } +); +SidebarTrigger.displayName = 'SidebarTrigger'; +SidebarTriggerButton.displayName = 'SidebarTriggerButton'; const SidebarRail = React.forwardRef>( ({ className, ...props }, ref) => { const { toggleSidebar } = useSidebar(); @@ -654,5 +675,6 @@ export { SidebarRail, SidebarSeparator, SidebarTrigger, + SidebarTriggerButton, useSidebar }; diff --git a/apps/web/components/ui/table.tsx b/apps/web/components/ui/table.tsx index fbf5986c2..ca52ea1c3 100644 --- a/apps/web/components/ui/table.tsx +++ b/apps/web/components/ui/table.tsx @@ -1,31 +1,35 @@ import * as React from 'react'; -import { clsxm } from '@app/utils'; +import { cn } from '@/lib/utils'; const Table = React.forwardRef>( ({ className, ...props }, ref) => ( -
    - +
    +
    ) ); Table.displayName = 'Table'; const TableHeader = React.forwardRef>( - ({ className, ...props }, ref) => + ({ className, ...props }, ref) => ); TableHeader.displayName = 'TableHeader'; const TableBody = React.forwardRef>( ({ className, ...props }, ref) => ( - + ) ); TableBody.displayName = 'TableBody'; const TableFooter = React.forwardRef>( ({ className, ...props }, ref) => ( - + tr]:last:border-b-0', className)} + {...props} + /> ) ); TableFooter.displayName = 'TableFooter'; @@ -34,7 +38,7 @@ const TableRow = React.forwardRef ( ) @@ -45,7 +49,7 @@ const TableHead = React.forwardRef (
    >( ({ className, ...props }, ref) => ( - + ) ); TableCell.displayName = 'TableCell'; const TableCaption = React.forwardRef>( ({ className, ...props }, ref) => ( -
    + ) ); TableCaption.displayName = 'TableCaption'; diff --git a/apps/web/lib/components/combobox/index.tsx b/apps/web/lib/components/combobox/index.tsx index 9ce3ed57e..d0faacc2b 100644 --- a/apps/web/lib/components/combobox/index.tsx +++ b/apps/web/lib/components/combobox/index.tsx @@ -1,121 +1,113 @@ -import * as React from "react"; -import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; -import { cn } from "lib/utils"; -import { Button } from "@components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@components/ui/popover"; +import * as React from 'react'; +import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'; +import { cn } from 'lib/utils'; +import { Button } from '@components/ui/button'; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; interface ComboboxProps { - items: T[] - itemToString: (item: T) => string - itemToValue: (item: T) => string - placeholder?: string - buttonWidth?: string - popoverWidth?: string - commandInputHeight?: string - noResultsText?: string - onChangeValue?: (value: T | null) => void - className?: string - popoverClassName?: string - selectedItem?: T | null + items: T[]; + itemToString: (item: T) => string; + itemToValue: (item: T) => string; + placeholder?: string; + buttonWidth?: string; + popoverWidth?: string; + commandInputHeight?: string; + noResultsText?: string; + onChangeValue?: (value: T | null) => void; + className?: string; + popoverClassName?: string; + selectedItem?: T | null; } export function CustomCombobox({ - items, - itemToString, - itemToValue, - placeholder = "Select item...", - buttonWidth = "w-[200px]", - commandInputHeight = "h-9", - noResultsText = "No item found.", - onChangeValue, - className, - popoverClassName, - selectedItem = null -}: ComboboxProps) { - const [open, setOpen] = React.useState(false) - const [value, setValue] = React.useState(selectedItem) - const [popoverWidth, setPopoverWidth] = React.useState(null); - const triggerRef = React.useRef(null); + items, + itemToString, + itemToValue, + placeholder = 'Select item...', + buttonWidth = 'w-[200px]', + commandInputHeight = 'h-9', + noResultsText = 'No item found.', + onChangeValue, + className, + popoverClassName, + selectedItem = null +}: Readonly>) { + const [open, setOpen] = React.useState(false); + const [value, setValue] = React.useState(selectedItem); + const [popoverWidth, setPopoverWidth] = React.useState(null); + const triggerRef = React.useRef(null); - const handleSelect = (currentValue: string) => { - const selectedItem = items.find(item => itemToValue(item) === currentValue) || null - setValue(selectedItem) - setOpen(false) - if (onChangeValue) { - onChangeValue(selectedItem) - } - } + const handleSelect = (currentValue: string) => { + const selectedItem = items.find((item) => itemToValue(item) === currentValue) || null; + setValue(selectedItem); + setOpen(false); + if (onChangeValue) { + onChangeValue(selectedItem); + } + }; - React.useEffect(() => { - if (triggerRef.current) { - setPopoverWidth(triggerRef.current.offsetWidth); - } - }, [triggerRef.current]); + React.useEffect(() => { + if (triggerRef.current) { + setPopoverWidth(triggerRef.current.offsetWidth); + } + }, [triggerRef.current]); + React.useEffect(() => { + setValue(selectedItem); + }, [selectedItem]); - React.useEffect(() => { - setValue(selectedItem); - }, [selectedItem]); - - return ( - - - - - - - - - {noResultsText} - - {items.map((item) => ( - handleSelect(itemToValue(item))} - > - {itemToString(item)} - - - ))} - - - - - - ) + return ( + + + + + + + + + {noResultsText} + + {items.map((item) => ( + handleSelect(itemToValue(item))} + > + {itemToString(item)} + + + ))} + + + + + + ); } diff --git a/apps/web/lib/components/pagination.tsx b/apps/web/lib/components/pagination.tsx index 5601da628..f6b4ab4b2 100644 --- a/apps/web/lib/components/pagination.tsx +++ b/apps/web/lib/components/pagination.tsx @@ -2,6 +2,7 @@ import { PaginationDropdown } from 'lib/settings/page-dropdown'; import { useTranslations } from 'next-intl'; import { Dispatch, SetStateAction } from 'react'; import ReactPaginate, { ReactPaginateProps } from 'react-paginate'; +import { cn } from '@/lib/utils'; type Props = { total: number; @@ -9,15 +10,24 @@ type Props = { itemOffset: number; endOffset: number; setItemsPerPage: Dispatch>; + className?: string; } & ReactPaginateProps; -export function Paginate({ total, itemsPerPage = 10, onPageChange, itemOffset, endOffset, setItemsPerPage }: Props) { +export function Paginate({ + total, + itemsPerPage = 10, + onPageChange, + itemOffset, + endOffset, + setItemsPerPage, + className +}: Props) { const t = useTranslations(); const pageCount: number = Math.ceil(total / itemsPerPage); return (
    {APP_LOGO_URL ? ( - EverTeams Logo + EverTeams Logo ) : ( void; + defaultValue?: TimePickerValue; + onChange?: (value: TimePickerValue) => void; } export function TimePicker({ onChange, defaultValue }: IPopoverTimePicker) { - const [time, setTime] = useState({ - hours: defaultValue?.hours, - minute: defaultValue?.minute, - meridiem: defaultValue?.meridiem, - }); - - const handleTimeChange = (newTime: any) => { - setTime(newTime); - onChange && onChange(newTime); - }; - return ( - - -
    - - {time.hours !== '--' && time.minute !== '--' ? `${time.hours}:${time.minute} ${time.meridiem}` : Time picker} -
    - - - - - - - ); + const [time, setTime] = useState({ + hours: defaultValue?.hours, + minute: defaultValue?.minute, + meridiem: defaultValue?.meridiem + }); + + const handleTimeChange = (newTime: any) => { + setTime(newTime); + onChange && onChange(newTime); + }; + return ( + + + + + + + + + ); } - - const TimePickerInput = ({ onTimeChange }: { onTimeChange: (_: any) => void }) => { - const [time, setTime] = useState({ - hours: 0, - minutes: 0, - meridiem: true, - }); - - const handleHoursClick = useCallback((index: number) => { - setTime((prev) => { - const newTime = { ...prev, hours: index }; - onTimeChange({ - ...newTime, - hours: String(index + 1).padStart(2, '0'), - minute: String(newTime.minutes).padStart(2, '0'), - meridiem: newTime.meridiem ? 'PM' : 'AM', - }); - return newTime; - }); - }, [onTimeChange]); - - const handleMinutesClick = useCallback((index: number) => { - setTime((prev) => { - const newTime = { ...prev, minutes: index }; - onTimeChange({ - ...newTime, - hours: String(newTime.hours + 1).padStart(2, '0'), - minute: String(index).padStart(2, '0'), - meridiem: newTime.meridiem ? 'PM' : 'AM', - }); - return newTime; - }); - }, [onTimeChange]); - - const handleMeridiemClick = useCallback((isAM: any) => { - setTime((prev) => { - const newTime = { ...prev, meridiem: isAM }; - onTimeChange({ - ...newTime, - hours: String(newTime.hours + 1).padStart(2, '0'), - minute: String(newTime.minutes).padStart(2, '0'), - meridiem: isAM ? 'PM' : 'AM', - }); - return newTime; - }); - }, [onTimeChange]); - - const renderButtons = (length: number, clickHandler: any, selectedIndex: number) => { - return Array.from({ length }, (_, index) => ( - clickHandler(index)} // deepscan-disable-line - variant={selectedIndex === index ? 'default' : 'outline'} - /> - )); - }; - - return ( -
    -
    -
    - {renderButtons(12, handleHoursClick, time.hours)} -
    -
    - {renderButtons(60, handleMinutesClick, time.minutes)} -
    -
    - handleMeridiemClick(!time.meridiem)} // deepscan-disable-line - variant={time.meridiem ? 'outline' : 'default'} - title={'AM'} - /> - handleMeridiemClick(!time.meridiem)} // deepscan-disable-line REACT_INEFFICIENT_PURE_COMPONENT_PROP - variant={time.meridiem ? 'default' : 'outline'} - title={'PM'} - /> -
    -
    -
    - ); + const [time, setTime] = useState({ + hours: 0, + minutes: 0, + meridiem: true + }); + + const handleHoursClick = useCallback( + (index: number) => { + setTime((prev) => { + const newTime = { ...prev, hours: index }; + onTimeChange({ + ...newTime, + hours: String(index + 1).padStart(2, '0'), + minute: String(newTime.minutes).padStart(2, '0'), + meridiem: newTime.meridiem ? 'PM' : 'AM' + }); + return newTime; + }); + }, + [onTimeChange] + ); + + const handleMinutesClick = useCallback( + (index: number) => { + setTime((prev) => { + const newTime = { ...prev, minutes: index }; + onTimeChange({ + ...newTime, + hours: String(newTime.hours + 1).padStart(2, '0'), + minute: String(index).padStart(2, '0'), + meridiem: newTime.meridiem ? 'PM' : 'AM' + }); + return newTime; + }); + }, + [onTimeChange] + ); + + const handleMeridiemClick = useCallback( + (isAM: any) => { + setTime((prev) => { + const newTime = { ...prev, meridiem: isAM }; + onTimeChange({ + ...newTime, + hours: String(newTime.hours + 1).padStart(2, '0'), + minute: String(newTime.minutes).padStart(2, '0'), + meridiem: isAM ? 'PM' : 'AM' + }); + return newTime; + }); + }, + [onTimeChange] + ); + + const renderButtons = (length: number, clickHandler: any, selectedIndex: number) => { + return Array.from({ length }, (_, index) => ( + clickHandler(index)} // deepscan-disable-line + variant={selectedIndex === index ? 'default' : 'outline'} + /> + )); + }; + + return ( +
    +
    +
    + {renderButtons(12, handleHoursClick, time.hours)} +
    +
    + {renderButtons(60, handleMinutesClick, time.minutes)} +
    +
    + handleMeridiemClick(!time.meridiem)} // deepscan-disable-line + variant={time.meridiem ? 'outline' : 'default'} + title={'AM'} + /> + handleMeridiemClick(!time.meridiem)} // deepscan-disable-line REACT_INEFFICIENT_PURE_COMPONENT_PROP + variant={time.meridiem ? 'default' : 'outline'} + title={'PM'} + /> +
    +
    +
    + ); }; export default TimePickerInput; - - interface TimerPickerButtonProps { - title?: string; - className?: string; - onClick?: () => void; - loading?: boolean; - variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null; + title?: string; + className?: string; + onClick?: () => void; + loading?: boolean; + variant?: 'link' | 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | null; } // eslint-disable-next-line react/display-name -const TimerPickerButton: React.FC = React.memo(({ - title = '', - className = 'border-none border-gray-100 dark:border-gray-700', - onClick = () => null, - loading = false, - variant = 'default', -}) => { - return ( - - ); -}); +const TimerPickerButton: React.FC = React.memo( + ({ + title = '', + className = 'border-none border-gray-100 dark:border-gray-700', + onClick = () => null, + loading = false, + variant = 'default' + }) => { + return ( + + ); + } +); diff --git a/apps/web/lib/features/activity/components/screenshot-details.tsx b/apps/web/lib/features/activity/components/screenshot-details.tsx index d61f47885..240890c51 100644 --- a/apps/web/lib/features/activity/components/screenshot-details.tsx +++ b/apps/web/lib/features/activity/components/screenshot-details.tsx @@ -1,6 +1,5 @@ 'use client'; -import React from 'react'; import { Modal, ProgressBar, Tooltip } from 'lib/components'; import { ITimerSlot } from '@app/interfaces/timer/ITimerSlot'; import ScreenshotItem from './screenshot-item'; @@ -23,7 +22,7 @@ const ScreenshotDetailsModal = ({ closeModal={closeModal} className="bg-white dark:bg-[#343434d4] p-4 rounded-lg lg:w-[60vw] xl:w-[50vw] 2xl:w-[40vw] m-8" > -
    +

    {new Date(slot.startedAt).toLocaleTimeString()} - {new Date(slot.stoppedAt).toLocaleTimeString()}

    diff --git a/apps/web/lib/features/integrations/calendar/calendar-component.tsx b/apps/web/lib/features/integrations/calendar/calendar-component.tsx index 9adbf35d6..dcbe13d3d 100644 --- a/apps/web/lib/features/integrations/calendar/calendar-component.tsx +++ b/apps/web/lib/features/integrations/calendar/calendar-component.tsx @@ -5,106 +5,112 @@ import timeGridPlugin from '@fullcalendar/timegrid'; import listPlugin from '@fullcalendar/list'; import interactionPlugin, { DateClickArg } from '@fullcalendar/interaction'; import { startOfYear, endOfYear } from 'date-fns'; -import { ClassNamesGenerator, DayCellContentArg, EventContentArg, EventDropArg, EventSourceInput } from '@fullcalendar/core'; +import { + ClassNamesGenerator, + DayCellContentArg, + EventContentArg, + EventDropArg, + EventSourceInput +} from '@fullcalendar/core'; import { ScrollArea } from '@components/ui/scroll-bar'; type CalendarComponentProps = { - events: EventSourceInput; - handleDateClick: (arg: DateClickArg) => void; - handleEventDrop: (arg: EventDropArg) => void; - renderEventContent: (arg: EventContentArg) => React.ReactNode; - dayCellClassNames: ClassNamesGenerator | undefined; - calendarRef: React.MutableRefObject; + events: EventSourceInput; + handleDateClick: (arg: DateClickArg) => void; + handleEventDrop: (arg: EventDropArg) => void; + renderEventContent: (arg: EventContentArg) => React.ReactNode; + dayCellClassNames: ClassNamesGenerator | undefined; + calendarRef: React.MutableRefObject; }; const CalendarComponent: React.FC = ({ - events, - handleDateClick, - handleEventDrop, - renderEventContent, - dayCellClassNames, - calendarRef, + events, + handleDateClick, + handleEventDrop, + renderEventContent, + dayCellClassNames, + calendarRef }) => { - return ( - - { - const start = startOfYear(currentDate); - const end = endOfYear(currentDate); - return { start, end }; - }, - titleFormat: { year: 'numeric' }, - eventClassNames: (info) => info.event.classNames, - }, - }} - dayCellClassNames={dayCellClassNames} - initialView='dayGridMonth' - events={events} - dateClick={handleDateClick} - eventDrop={handleEventDrop} - eventContent={renderEventContent} - editable={true} - dragScroll={true} - locale={"pt-br"} - timeZone={"UTF"} - allDaySlot={false} - nowIndicator={true} - themeSystem='bootstrap' - contentHeight='auto' - select={(selected) => { - console.log(selected.start) - }} - eventResize={(resize) => { - console.log(resize.el.COMMENT_NODE) - }} + return ( + + { + const start = startOfYear(currentDate); + const end = endOfYear(currentDate); + return { start, end }; + }, + titleFormat: { year: 'numeric' }, + eventClassNames: (info) => info.event.classNames + } + }} + dayCellClassNames={dayCellClassNames} + initialView="dayGridMonth" + events={events} + dateClick={handleDateClick} + eventDrop={handleEventDrop} + eventContent={renderEventContent} + editable={true} + dragScroll={true} + locale={'pt-br'} + timeZone={'UTF'} + allDaySlot={false} + nowIndicator={true} + themeSystem="bootstrap" + contentHeight="auto" + select={(selected) => { + console.log(selected.start); + }} + eventResize={(resize) => { + console.log(resize.el.COMMENT_NODE); + }} - // dayPopoverFormat={} - /> - - - ); + // dayPopoverFormat={} + /> + + + ); }; export default CalendarComponent; 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 d8420c39e..0b5f6ac59 100644 --- a/apps/web/lib/features/task/daily-plan/future-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/future-tasks.tsx @@ -1,14 +1,5 @@ -import { - formatDayPlanDate, - handleDragAndDrop, - tomorrowDate -} from '@app/helpers'; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger -} from '@components/ui/accordion'; +import { formatDayPlanDate, handleDragAndDrop, tomorrowDate } from '@app/helpers'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; import { EmptyPlans, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; import { Button } from '@components/ui/button'; @@ -27,189 +18,162 @@ import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { useDateRange } from '@app/hooks/useDateRange'; export function FutureTasks({ profile }: { profile: any }) { - const { - deleteDailyPlan, - deleteDailyPlanLoading, - futurePlans - } = useDailyPlan(); - const canSeeActivity = useCanSeeActivityScreen(); - const [popupOpen, setPopupOpen] = useState(false); + const { deleteDailyPlan, deleteDailyPlanLoading, futurePlans } = useDailyPlan(); + const canSeeActivity = useCanSeeActivityScreen(); + const [popupOpen, setPopupOpen] = useState(false); - const [currentDeleteIndex, setCurrentDeleteIndex] = useState(0); - const { setDate, date } = useDateRange( - window.localStorage.getItem('daily-plan-tab') - ); - const [futureDailyPlanTasks, setFutureDailyPlanTasks] = useState< - IDailyPlan[] - >(futurePlans); - useEffect(() => { - setFutureDailyPlanTasks(filterDailyPlan(date as any, futurePlans)); - }, [date, setDate, futurePlans]); - const view = useAtomValue(dailyPlanViewHeaderTabs); + const [currentDeleteIndex, setCurrentDeleteIndex] = useState(0); + const { setDate, date } = useDateRange(window.localStorage.getItem('daily-plan-tab')); + const [futureDailyPlanTasks, setFutureDailyPlanTasks] = useState(futurePlans); + useEffect(() => { + setFutureDailyPlanTasks(filterDailyPlan(date as any, futurePlans)); + }, [date, setDate, futurePlans]); + const view = useAtomValue(dailyPlanViewHeaderTabs); - return ( -
    - {futureDailyPlanTasks?.length > 0 ? ( - - handleDragAndDrop( - result, - futureDailyPlanTasks, - setFutureDailyPlanTasks - ) - } - > - - {futureDailyPlanTasks.map((plan, index) => ( - - -
    -
    - {formatDayPlanDate(plan.date.toString())} ( - {plan.tasks?.length}) -
    - -
    -
    - - - - {(provided) => ( -
      - {plan.tasks?.map((task, index) => - view === 'CARDS' ? ( - - {(provided) => ( -
      - -
      - )} -
      - ) : ( - - {(provided) => ( -
      - -
      - )} -
      - ) - )} - <>{provided.placeholder} - {canSeeActivity ? ( -
      - { - setPopupOpen((prev) => !prev); - setCurrentDeleteIndex(index); - }} - variant="outline" - className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" - > - Delete this plan - - } - > - {/*button confirm*/} - - {/*button cancel*/} - - -
      - ) : ( - <> - )} -
    - )} -
    -
    -
    - ))} -
    -
    - ) : ( - - )} -
    - ); + return ( +
    + {futureDailyPlanTasks?.length > 0 ? ( + handleDragAndDrop(result, futureDailyPlanTasks, setFutureDailyPlanTasks)} + > + + {futureDailyPlanTasks.map((plan, index) => ( + + +
    +
    + {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
    + +
    +
    + + + + {(provided) => ( +
      + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
      + +
      + )} +
      + ) : ( + + {(provided) => ( +
      + +
      + )} +
      + ) + )} + <>{provided.placeholder} + {canSeeActivity ? ( +
      + { + setPopupOpen((prev) => !prev); + setCurrentDeleteIndex(index); + }} + variant="outline" + className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" + > + Delete this plan + + } + > + {/*button confirm*/} + + {/*button cancel*/} + + +
      + ) : ( + <> + )} +
    + )} +
    +
    +
    + ))} +
    +
    + ) : ( + + )} +
    + ); } diff --git a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx index a0bec60b5..bf5d565ea 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -6,125 +6,108 @@ import { useAtomValue } from 'jotai'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; import TaskBlockCard from '../task-block-card'; import { clsxm } from '@app/utils'; -import { - DragDropContext, - Draggable, - Droppable, - DroppableProvided -} from 'react-beautiful-dnd'; +import { DragDropContext, Draggable, Droppable, DroppableProvided } from 'react-beautiful-dnd'; import { useState } from 'react'; import { ITeamTask } from '@app/interfaces'; import { handleDragAndDropDailyOutstandingAll } from '@app/helpers'; interface OutstandingAll { - profile: any; + profile: any; } export function OutstandingAll({ profile }: OutstandingAll) { - const { outstandingPlans } = useDailyPlan(); - const view = useAtomValue(dailyPlanViewHeaderTabs); - const displayedTaskId = new Set(); + const { outstandingPlans } = useDailyPlan(); + const view = useAtomValue(dailyPlanViewHeaderTabs); + const displayedTaskId = new Set(); - const tasks = outstandingPlans - .map((plan) => plan.tasks) - .reduce((red, curr) => red?.concat(curr || []), []); - const [task, setTask] = useState(() => tasks ?? []); + const tasks = outstandingPlans.map((plan) => plan.tasks).reduce((red, curr) => red?.concat(curr || []), []); + const [task, setTask] = useState(() => tasks ?? []); - return ( -
    - + return ( +
    + - {tasks && tasks?.length > 0 ? ( - <> - - handleDragAndDropDailyOutstandingAll(result, task, setTask) - } - > - {/* */} - - {(provided: DroppableProvided) => ( -
      - {tasks?.map((task, index) => { - //If the task is already displayed, skip it - if (displayedTaskId.has(task.id)) { - return null; - } - // Add the task to the Set to avoid displaying it again - displayedTaskId.add(task.id); - return view === 'CARDS' ? ( - - {(provided) => ( -
      - -
      - )} -
      - ) : ( - - {(provided) => ( -
      - -
      - )} -
      - ); - })} -
    - )} -
    -
    - - ) : ( - - )} -
    - ); + {tasks && tasks?.length > 0 ? ( + <> + handleDragAndDropDailyOutstandingAll(result, task, setTask)} + > + {/* */} + + {(provided: DroppableProvided) => ( +
      + {tasks?.map((task, index) => { + //If the task is already displayed, skip it + if (displayedTaskId.has(task.id)) { + return null; + } + // Add the task to the Set to avoid displaying it again + displayedTaskId.add(task.id); + return view === 'CARDS' ? ( + + {(provided) => ( +
      + +
      + )} +
      + ) : ( + + {(provided) => ( +
      + +
      + )} +
      + ); + })} +
    + )} +
    +
    + + ) : ( + + )} +
    + ); } 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 eaf8c8fdc..b3f50d7a4 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx @@ -1,10 +1,5 @@ import { formatDayPlanDate, handleDragAndDrop } from '@app/helpers'; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger -} from '@components/ui/accordion'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; import { EmptyPlans, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; import { useDailyPlan } from '@app/hooks'; @@ -18,130 +13,111 @@ import { useState } from 'react'; import { IDailyPlan } from '@app/interfaces'; interface IOutstandingFilterDate { - profile: any; + profile: any; } export function OutstandingFilterDate({ profile }: IOutstandingFilterDate) { - const { outstandingPlans } = useDailyPlan(); - const view = useAtomValue(dailyPlanViewHeaderTabs); - const [outstandingTasks, setOutstandingTasks] = useState( - outstandingPlans - ); - return ( -
    - {outstandingTasks?.length > 0 ? ( - - handleDragAndDrop(result, outstandingTasks, setOutstandingTasks) - } - > - new Date(plan.date).toISOString().split('T')[0] - )} - > - {outstandingTasks?.map((plan) => ( - - -
    -
    - {formatDayPlanDate(plan.date.toString())} ( - {plan.tasks?.length}) -
    - -
    -
    - - {/* Plan header */} - - - {(provided) => ( -
      - {plan.tasks?.map((task, index) => - view === 'CARDS' ? ( - - {(provided) => ( -
      - -
      - )} -
      - ) : ( - - {(provided) => ( -
      - -
      - )} -
      - ) - )} -
    - )} - {/* <>{provided.placeholder} */} - {/* Plan tasks list */} -
    -
    -
    - ))} -
    -
    - ) : ( - - )} -
    - ); + const { outstandingPlans } = useDailyPlan(); + const view = useAtomValue(dailyPlanViewHeaderTabs); + const [outstandingTasks, setOutstandingTasks] = useState(outstandingPlans); + return ( +
    + {outstandingTasks?.length > 0 ? ( + handleDragAndDrop(result, outstandingTasks, setOutstandingTasks)} + > + new Date(plan.date).toISOString().split('T')[0])} + > + {outstandingTasks?.map((plan) => ( + + +
    +
    + {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
    + +
    +
    + + {/* Plan header */} + + + {(provided) => ( +
      + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
      + +
      + )} +
      + ) : ( + + {(provided) => ( +
      + +
      + )} +
      + ) + )} +
    + )} + {/* <>{provided.placeholder} */} + {/* Plan tasks list */} +
    +
    +
    + ))} +
    +
    + ) : ( + + )} +
    + ); } diff --git a/apps/web/lib/features/task/daily-plan/past-tasks.tsx b/apps/web/lib/features/task/daily-plan/past-tasks.tsx index be61222d1..29ce71c3e 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -1,19 +1,6 @@ -import { - formatDayPlanDate, - handleDragAndDrop, - yesterdayDate -} from '@app/helpers'; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger -} from '@components/ui/accordion'; -import { - EmptyPlans, - FilterTabs, - PlanHeader -} from 'lib/features/user-profile-plans'; +import { formatDayPlanDate, handleDragAndDrop, yesterdayDate } from '@app/helpers'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; +import { EmptyPlans, FilterTabs, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; import { useDailyPlan } from '@app/hooks'; import { useAtomValue } from 'jotai'; @@ -24,155 +11,127 @@ import TaskBlockCard from '../task-block-card'; import { filterDailyPlan } from '@app/hooks/useFilterDateRange'; import { useEffect, useState } from 'react'; import { IDailyPlan } from '@app/interfaces'; -import { - DragDropContext, - Draggable, - Droppable, - DroppableProvided, - DroppableStateSnapshot -} from 'react-beautiful-dnd'; +import { DragDropContext, Draggable, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; import { useDateRange } from '@app/hooks/useDateRange'; -export function PastTasks({ - profile, - currentTab = 'Past Tasks' -}: { - profile: any; - currentTab?: FilterTabs; -}) { - const { pastPlans } = useDailyPlan(); +export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any; currentTab?: FilterTabs }) { + const { pastPlans } = useDailyPlan(); - const view = useAtomValue(dailyPlanViewHeaderTabs); - const [pastTasks, setPastTasks] = useState(pastPlans); - const { date } = useDateRange(window.localStorage.getItem('daily-plan-tab')); + const view = useAtomValue(dailyPlanViewHeaderTabs); + const [pastTasks, setPastTasks] = useState(pastPlans); + const { date } = useDateRange(window.localStorage.getItem('daily-plan-tab')); - useEffect(() => { - setPastTasks(filterDailyPlan(date as any, pastPlans)); - }, [date, pastPlans]); + useEffect(() => { + setPastTasks(filterDailyPlan(date as any, pastPlans)); + }, [date, pastPlans]); - return ( -
    - {pastTasks?.length > 0 ? ( - - handleDragAndDrop(result, pastPlans, setPastTasks) - } - > - - {pastTasks?.map((plan) => ( - - -
    -
    - {formatDayPlanDate(plan.date.toString())} ( - {plan.tasks?.length}) -
    - -
    -
    - - {/* Plan header */} - - - {( - provided: DroppableProvided, - snapshot: DroppableStateSnapshot - ) => ( -
      - {plan.tasks?.map((task, index) => - view === 'CARDS' ? ( - - {(provided) => ( -
      - -
      - )} -
      - ) : ( - - {(provided) => ( -
      - -
      - )} -
      - ) - )} -
    - )} -
    -
    -
    - ))} -
    -
    - ) : ( - - )} -
    - ); + return ( +
    + {pastTasks?.length > 0 ? ( + handleDragAndDrop(result, pastPlans, setPastTasks)}> + + {pastTasks?.map((plan) => ( + + +
    +
    + {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
    + +
    +
    + + {/* Plan header */} + + + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( +
      + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
      + +
      + )} +
      + ) : ( + + {(provided) => ( +
      + +
      + )} +
      + ) + )} +
    + )} +
    +
    +
    + ))} +
    +
    + ) : ( + + )} +
    + ); } diff --git a/apps/web/lib/features/task/task-status.tsx b/apps/web/lib/features/task/task-status.tsx index 1feab6c82..b0967a01e 100644 --- a/apps/web/lib/features/task/task-status.tsx +++ b/apps/web/lib/features/task/task-status.tsx @@ -1076,7 +1076,7 @@ export function StatusDropdown({ {items.map((item, i) => { const item_value = item?.value || item?.name; @@ -1252,7 +1252,7 @@ export function MultipleStatusDropdown({ shadow="bigger" className="p-4 md:p-4 shadow-xlcard dark:shadow-lgcard-white dark:bg-[#1B1D22] dark:border dark:border-[#FFFFFF33] flex flex-col" > -
    +
    {items.map((item, i) => { const item_value = item.value || item.name; return ( diff --git a/apps/web/lib/features/team-members.tsx b/apps/web/lib/features/team-members.tsx index 0a922bb9c..97276459f 100644 --- a/apps/web/lib/features/team-members.tsx +++ b/apps/web/lib/features/team-members.tsx @@ -19,7 +19,7 @@ type TeamMembersProps = { kanbanView?: IssuesView; }; -export function TeamMembers({ publicTeam = false, kanbanView: view = IssuesView.CARDS }: TeamMembersProps) { +export function TeamMembers({ publicTeam = false, kanbanView: view = IssuesView.CARDS }: Readonly) { const { user } = useAuthenticateUser(); const activeFilter = useAtomValue(taskBlockFilterState); const fullWidth = useAtomValue(fullWidthState); @@ -93,7 +93,7 @@ export function TeamMembersView({ switch (true) { case members.length === 0: teamMembersView = ( - +
    @@ -110,7 +110,7 @@ export function TeamMembersView({ teamMembersView = ( <> {/* */} - + + + + @@ -444,8 +444,9 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil return (
    {/* Planned Time */} diff --git a/apps/web/lib/layout/main-layout.tsx b/apps/web/lib/layout/main-layout.tsx index 5ad0c9cea..ea9dd33c9 100644 --- a/apps/web/lib/layout/main-layout.tsx +++ b/apps/web/lib/layout/main-layout.tsx @@ -33,7 +33,7 @@ export function MainLayout({ }: Props) { const fullWidth = useAtomValue(fullWidthState); return ( -
    +