From 7c6a36d7082b45b3616b9d6d340360adc1fd0c97 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Mon, 15 Jul 2024 15:19:38 +0200 Subject: [PATCH 01/11] fix: passcode signin issue --- .../app/[locale]/auth/passcode/component.tsx | 2 +- .../services/client/api/auth/invite-accept.ts | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/web/app/[locale]/auth/passcode/component.tsx b/apps/web/app/[locale]/auth/passcode/component.tsx index 05e51941a..87676b98d 100644 --- a/apps/web/app/[locale]/auth/passcode/component.tsx +++ b/apps/web/app/[locale]/auth/passcode/component.tsx @@ -335,7 +335,7 @@ function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode } if (form.workspaces.length === 1 && currentTeams?.length === 1) { setSelectedTeam(currentTeams[0].team_id); } else { - const lastSelectedTeam = window.localStorage.getItem(LAST_WORSPACE_AND_TEAM) || currentTeams[0].team_id; + const lastSelectedTeam = window.localStorage.getItem(LAST_WORSPACE_AND_TEAM) || currentTeams[0]?.team_id; const lastSelectedWorkspace = form.workspaces.findIndex((workspace) => workspace.current_teams.find((team) => team.team_id === lastSelectedTeam) diff --git a/apps/web/app/services/client/api/auth/invite-accept.ts b/apps/web/app/services/client/api/auth/invite-accept.ts index bee527471..75b3be2c7 100644 --- a/apps/web/app/services/client/api/auth/invite-accept.ts +++ b/apps/web/app/services/client/api/auth/invite-accept.ts @@ -205,13 +205,24 @@ export function signInWorkspaceAPI(email: string, token: string) { * @returns */ export async function signInEmailConfirmGauzy(email: string, code: string) { - const loginResponse = await signInEmailCodeConfirmGauzy(email, code); + let loginResponse; + + try { + loginResponse = await signInEmailCodeConfirmGauzy(email, code); + } catch (error) { + console.error('Error in signInEmailCodeConfirmation:', error); + } if (loginResponse) { return loginResponse; } - return signInEmailConfirmAPI({ email, code }); + try { + const signinResponse = await signInEmailConfirmAPI({ email, code }); + return signinResponse; + } catch (error) { + return Promise.reject(error); + } } /** @@ -219,7 +230,12 @@ export async function signInEmailConfirmGauzy(email: string, code: string) { */ export async function signInWorkspaceGauzy(params: { email: string; token: string; teamId: string; code?: string }) { if (params.code) { - const loginResponse = await signInEmailCodeConfirmGauzy(params.email, params.code); + let loginResponse; + try { + loginResponse = await signInEmailCodeConfirmGauzy(params.email, params.code); + } catch (error) { + console.error('Error in signInWorkspaces', error); + } if (loginResponse) { return loginResponse; From cb5d3991f63e173b04c89d221dfe9a46d2fe7edd Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:37:44 +0200 Subject: [PATCH 02/11] [Feat] Filter Daily plans By date range (#2734) * feat: daily plan filter by date range * fix: error daily plan filter by date range al * fix: daily plan delete plan | open multiple popup * Delete duplicated file --------- Co-authored-by: Ruslan Konviser Co-authored-by: cedric karungu --- apps/web/app/helpers/date.ts | 13 ++++ apps/web/app/hooks/useDateRange.ts | 17 +++++ apps/web/app/hooks/useFilterDateRange.ts | 74 +++++++++++++++++++ apps/web/app/stores/daily-plan.ts | 68 ++++++++++++++++- .../features/task/daily-plan/future-tasks.tsx | 18 +++-- .../features/task/daily-plan/past-tasks.tsx | 7 +- .../web/lib/features/task/task-date-range.tsx | 71 ++++++++++++++++++ apps/web/lib/features/task/task-filters.tsx | 20 ++++- apps/web/lib/features/user-profile-plans.tsx | 29 +++++--- apps/web/tsconfig.json | 28 +++++-- 10 files changed, 319 insertions(+), 26 deletions(-) create mode 100644 apps/web/app/hooks/useDateRange.ts create mode 100644 apps/web/app/hooks/useFilterDateRange.ts create mode 100644 apps/web/lib/features/task/task-date-range.tsx diff --git a/apps/web/app/helpers/date.ts b/apps/web/app/helpers/date.ts index ace5da630..5aa529fd2 100644 --- a/apps/web/app/helpers/date.ts +++ b/apps/web/app/helpers/date.ts @@ -150,3 +150,16 @@ export const formatIntegerToHour = (number: number) => { return formattedHour; }; + + +export const isTestDateRange = (itemDate: Date, from?: Date, to?: Date) => { + if (from && to) { + return itemDate >= from && itemDate <= to; + } else if (from) { + return itemDate >= from; + } else if (to) { + return itemDate <= to; + } else { + return true; // or false, depending on your default logic + } +} diff --git a/apps/web/app/hooks/useDateRange.ts b/apps/web/app/hooks/useDateRange.ts new file mode 100644 index 000000000..38e3cec61 --- /dev/null +++ b/apps/web/app/hooks/useDateRange.ts @@ -0,0 +1,17 @@ +import { dateRangeAllPlanState, dateRangeFuturePlanState, dateRangePastPlanState } from "@app/stores"; +import { useRecoilState } from "recoil"; + +export const useDateRange = (tab: string | any) => { + const [dateFuture, setDateFuture] = useRecoilState(dateRangeFuturePlanState); + const [dateAllPlan, setDateAllPlan] = useRecoilState(dateRangeAllPlanState); + const [datePastPlan, setDatePastPlan] = useRecoilState(dateRangePastPlanState); + switch (tab) { + case 'Future Tasks': + return { date: dateFuture, setDate: setDateFuture }; + case 'Past Tasks': + return { date: datePastPlan, setDate: setDatePastPlan }; + case 'All Tasks': + default: + return { date: dateAllPlan, setDate: setDateAllPlan }; + } +} diff --git a/apps/web/app/hooks/useFilterDateRange.ts b/apps/web/app/hooks/useFilterDateRange.ts new file mode 100644 index 000000000..869d201bf --- /dev/null +++ b/apps/web/app/hooks/useFilterDateRange.ts @@ -0,0 +1,74 @@ +'use client'; + +import { IDailyPlan } from '@app/interfaces' +import { dateRangeAllPlanState, dateRangeFuturePlanState, dateRangePastPlanState, filteredAllPlanDataState, filteredFuturePlanDataState, filteredPastPlanDataState, originalAllPlanState, originalFuturePlanState, originalPastPlanDataState } from '@app/stores'; +import { useEffect, useMemo } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; +/** + *custom filter the data with date range + * + * @export + * @param {IDailyPlan[]} itemsDailyPlan + * @param {('future' | 'past' | 'all')} [typeItems] + * @return {*} + */ +export function useFilterDateRange(itemsDailyPlan: IDailyPlan[], typeItems?: 'future' | 'past' | 'all') { + + const [dateAllPlan, setDateAllPlan] = useRecoilState(dateRangeAllPlanState); + const [datePastPlan, setDatePastPlan] = useRecoilState(dateRangePastPlanState); + const [dateFuture, setDateFuture] = useRecoilState(dateRangeFuturePlanState); + + const [originalAllPlanData, setOriginalAllPlanState] = useRecoilState(originalAllPlanState); + const [originalPastPlanData, setOriginalPastPlanData] = useRecoilState(originalPastPlanDataState); + const [originalFuturePlanData, setOriginalFuturePlanData] = useRecoilState(originalFuturePlanState); + + const filteredAllPlanData = useRecoilValue(filteredAllPlanDataState); + const filteredPastPlanData = useRecoilValue(filteredPastPlanDataState); + const filteredFuturePlanData = useRecoilValue(filteredFuturePlanDataState); + + // useEffect(() => { + // if (!itemsDailyPlan) return; + + // if (typeItems === 'future') { + // setOriginalFuturePlanData(itemsDailyPlan); + // } else if (typeItems === 'past') { + // setOriginalPastPlanData(itemsDailyPlan); + // } else if (typeItems === 'all') { + // setOriginalAllPlanState(itemsDailyPlan); + // } + // }, [itemsDailyPlan, dateFuture, datePastPlan, dateAllPlan, typeItems, setOriginalAllPlanState, setOriginalFuturePlanData, setOriginalAllPlanState]); + + const updateOriginalPlanData = useMemo(() => (data: IDailyPlan[]) => { + switch (typeItems) { + case 'future': + setOriginalFuturePlanData(data); + break; + case 'past': + setOriginalPastPlanData(data); + break; + case 'all': + setOriginalAllPlanState(data); + break; + default: + break; + } + }, [typeItems, setOriginalAllPlanState, setOriginalFuturePlanData, setOriginalPastPlanData]); + + useEffect(() => { + if (!itemsDailyPlan) return; + updateOriginalPlanData(itemsDailyPlan); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [updateOriginalPlanData, dateAllPlan, datePastPlan, dateFuture]); + + return { + filteredAllPlanData, + filteredPastPlanData, + filteredFuturePlanData, + originalAllPlanData, + originalFuturePlanData, + originalPastPlanData, + setDateAllPlan, + setDateFuture, + setDatePastPlan, + } +} diff --git a/apps/web/app/stores/daily-plan.ts b/apps/web/app/stores/daily-plan.ts index ab4b53e8b..1daea096c 100644 --- a/apps/web/app/stores/daily-plan.ts +++ b/apps/web/app/stores/daily-plan.ts @@ -1,5 +1,12 @@ -import { atom, selector } from 'recoil'; +import { atom, RecoilState, selector } from 'recoil'; import { IDailyPlan, PaginationResponse } from '@app/interfaces'; +import { addDays } from 'date-fns'; +import { DateRange } from 'react-day-picker'; +import { isTestDateRange } from '@app/helpers'; + +const today = new Date(); +const oneWeekAgo = new Date(); +oneWeekAgo.setDate(today.getDate() - 7); export const dailyPlanListState = atom>({ key: 'dailyPlanListState', @@ -44,3 +51,62 @@ export const activeDailyPlanState = selector({ return dailyPlans.items.find((plan) => plan.id === activeId) || dailyPlans.items[0] || null; } }); +const createDailyPlanCountFilterAtom = (key: string | any) => atom( + { + key, + default: 0 + } +) + +const createDailyPlanAtom = (key: string | any) => atom({ + key, + default: [], +}); + +const createDateRangeAtom = (key: string | any) => atom({ + key, + default: { + from: oneWeekAgo, + to: addDays(today, 3), + }, +}); + +const createFilteredDailyPlanDataSelector = (key: string | any, dateRangeState: RecoilState, originalDataState: RecoilState) => selector({ + key, + get: ({ get }) => { + const dateRange = get(dateRangeState); + const data = get(originalDataState); + if (!dateRange || !data.length) return data; + const { from, to } = dateRange; + return data.filter((plan) => { + const itemDate = new Date(plan.date); + return isTestDateRange(itemDate, from, to); + }); + }, +}); +export const dataDailyPlanCountFilterState = createDailyPlanCountFilterAtom('dataDailyPlanCountFilterState'); +export const dateRangePastPlanState = createDateRangeAtom('dateRangePastPlanState'); +export const dateRangeFuturePlanState = createDateRangeAtom('dateRangeFuturePlanState'); +export const dateRangeAllPlanState = createDateRangeAtom('dateRangeAllPlanState'); + +export const originalFuturePlanState = createDailyPlanAtom('originalFuturePlanState'); +export const originalAllPlanState = createDailyPlanAtom('originalAllPlanState'); +export const originalPastPlanDataState = createDailyPlanAtom('originalPastPlanDataState'); + +export const filteredPastPlanDataState = createFilteredDailyPlanDataSelector( + 'filteredPastPlanDataState', + dateRangePastPlanState, + originalPastPlanDataState +); + +export const filteredFuturePlanDataState = createFilteredDailyPlanDataSelector( + 'filteredFuturePlanDataState', + dateRangeFuturePlanState, + originalFuturePlanState +); + +export const filteredAllPlanDataState = createFilteredDailyPlanDataSelector( + 'filteredAllPlanDataState', + dateRangeAllPlanState, + originalAllPlanState +); 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 ff4af85e1..00271eb1e 100644 --- a/apps/web/lib/features/task/daily-plan/future-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/future-tasks.tsx @@ -12,23 +12,26 @@ import { clsxm } from '@app/utils'; import { HorizontalSeparator } from 'lib/components'; import { useState } from 'react'; import { AlertPopup } from 'lib/components'; +import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; export function FutureTasks({ profile }: { profile: any }) { const { deleteDailyPlan, deleteDailyPlanLoading, futurePlans } = useDailyPlan(); const canSeeActivity = useCanSeeActivityScreen(); const [popupOpen, setPopupOpen] = useState(false); + const { filteredFuturePlanData: filteredFuturePlanData } = useFilterDateRange(futurePlans, 'future'); + const [currentDelete, setCurrentDelete] = useState(""); const view = useRecoilValue(dailyPlanViewHeaderTabs); return (
- {futurePlans.length > 0 ? ( + {filteredFuturePlanData.length > 0 ? ( - {futurePlans.map((plan) => ( + {filteredFuturePlanData.map((plan, index) => ( setPopupOpen(true)} + onClick={() => { + setCurrentDelete(plan?.id ?? "") + setPopupOpen(true) + }} 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" > @@ -95,7 +101,9 @@ export function FutureTasks({ profile }: { profile: any }) { {/*button confirm*/} + + + + + +
+ ) +} diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index 72b3d2c22..d8cff29ce 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -20,6 +20,8 @@ import { TaskLabelsDropdown, TaskPropertiesDropdown, TaskSizesDropdown, TaskStat import { useTranslations } from 'next-intl'; import { SettingFilterIcon } from 'assets/svg'; import { DailyPlanFilter } from './daily-plan/daily-plan-filter'; +import { useDateRange } from '@app/hooks/useDateRange'; +import { TaskDatePickerWithRange } from './task-date-range'; type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; type ITabs = { @@ -174,9 +176,9 @@ export function useTaskFilter(profile: I_UserProfilePage) { .every((k) => { return k === 'label' ? intersection( - statusFilters[k], - task['tags'].map((item) => item.name) - ).length === statusFilters[k].length + statusFilters[k], + task['tags'].map((item) => item.name) + ).length === statusFilters[k].length : statusFilters[k].includes(task[k]); }); }); @@ -211,6 +213,7 @@ export type I_TaskFilter = ReturnType; type Props = { hook: I_TaskFilter; profile: I_UserProfilePage }; export function TaskFilter({ className, hook, profile }: IClassName & Props) { + return (
{ + setDailyPlanTab(window.localStorage.getItem('daily-plan-tab') || "Future Tasks") + }, [dailyPlanTab]) return (
@@ -404,7 +414,9 @@ export function TaskStatusFilter({ hook, employeeId }: { hook: I_TaskFilter; emp /> {hook.tab === 'dailyplan' && } - + {['Future Tasks', 'Past Tasks', 'All Tasks'].includes(dailyPlanTab) && ( + setDate(range)} label='Planned date' /> + )}
@@ -138,27 +142,31 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current let filteredPlans: IDailyPlan[] = []; const { deleteDailyPlan, deleteDailyPlanLoading, sortedPlans, todayPlan } = useDailyPlan(); const [popupOpen, setPopupOpen] = useState(false); + const [currentId, setCurrentId] = useState(""); filteredPlans = sortedPlans; if (currentTab === 'Today Tasks') filteredPlans = todayPlan; const canSeeActivity = useCanSeeActivityScreen(); + const { filteredAllPlanData: filterAllPlanData } = useFilterDateRange(filteredPlans, 'all'); + const filterPlans: IDailyPlan[] = currentTab === 'All Tasks' ? filterAllPlanData : filteredPlans; const view = useRecoilValue(dailyPlanViewHeaderTabs); + return (
- {filteredPlans?.length > 0 ? ( + {filterPlans?.length > 0 ? ( new Date(plan.date).toISOString().split('T')[0])[0]] + : [filterPlans?.map((plan) => new Date(plan.date).toISOString().split('T')[0])[0]] } > - {filteredPlans?.map((plan) => ( + {filterPlans?.map((plan) => ( setPopupOpen(true)} + onClick={() => { + setCurrentId(plan.id ?? "") + setPopupOpen(true) + }} 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" > diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index ea316f538..9e750b5ec 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -2,7 +2,12 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "target": "ES2015", - "lib": ["dom", "dom.iterable", "esnext", "es2015"], + "lib": [ + "dom", + "dom.iterable", + "esnext", + "es2015" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -16,8 +21,12 @@ "jsx": "preserve", "baseUrl": ".", "paths": { - "@components/*": ["./components/*"], - "@app/*": ["./app/*"] + "@components/*": [ + "./components/*" + ], + "@app/*": [ + "./app/*" + ] }, "incremental": true, "plugins": [ @@ -26,6 +35,15 @@ } ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } From c31986a6df5602446ecc1d2d198740b4ca707e2d Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Mon, 15 Jul 2024 18:25:18 +0200 Subject: [PATCH 03/11] fix: daily plan creation, filters and sort etc. --- .../web/app/services/client/api/daily-plan.ts | 6 +- .../services/server/requests/daily-plan.ts | 6 +- .../create-daily-plan-form-modal.tsx | 12 +-- .../task/daily-plan/outstanding-all.tsx | 75 +++++++++---------- .../web/lib/features/task/task-block-card.tsx | 6 +- apps/web/lib/features/task/task-card.tsx | 6 +- 6 files changed, 55 insertions(+), 56 deletions(-) diff --git a/apps/web/app/services/client/api/daily-plan.ts b/apps/web/app/services/client/api/daily-plan.ts index a99f1596e..6296dcfb3 100644 --- a/apps/web/app/services/client/api/daily-plan.ts +++ b/apps/web/app/services/client/api/daily-plan.ts @@ -14,7 +14,7 @@ export function getAllDayPlansAPI() { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - const relations = ['employee', 'tasks', 'employee.user']; + const relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user']; const obj = { 'where[organizationId]': organizationId, @@ -33,7 +33,7 @@ export function getMyDailyPlansAPI() { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - const relations = ['employee', 'tasks']; + const relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user']; const obj = { 'where[organizationId]': organizationId, @@ -52,7 +52,7 @@ export function getDayPlansByEmployeeAPI(employeeId?: string) { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - const relations = ['employee', 'tasks']; + const relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user']; const obj = { 'where[organizationId]': organizationId, diff --git a/apps/web/app/services/server/requests/daily-plan.ts b/apps/web/app/services/server/requests/daily-plan.ts index 11f451d4a..6357f7246 100644 --- a/apps/web/app/services/server/requests/daily-plan.ts +++ b/apps/web/app/services/server/requests/daily-plan.ts @@ -7,7 +7,7 @@ export function getAllDayPlans({ organizationId, tenantId, bearer_token, - relations = ['employee', 'tasks'] + relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user'] }: { organizationId: string; tenantId: string; @@ -36,7 +36,7 @@ export function getMyDailyPlansRequest({ organizationId, tenantId, bearer_token, - relations = ['employee', 'tasks'] + relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user'] }: { organizationId: string; tenantId: string; @@ -66,7 +66,7 @@ export function getDayPlansByEmployee({ organizationId, tenantId, bearer_token, - relations = ['employee', 'tasks'] + relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user'] }: { employeeId: string; organizationId: string; diff --git a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx index 3264fe250..a60d3dfac 100644 --- a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx +++ b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'; import { useForm } from 'react-hook-form'; import { DailyPlanStatusEnum, IDailyPlanMode, IOrganizationTeamList, OT_Member } from '@app/interfaces'; import { useAuthenticateUser, useDailyPlan, useOrganizationTeams } from '@app/hooks'; -import { Avatar, Card, InputField, Modal, Text } from 'lib/components'; +import { Avatar, Card, Modal, Text } from 'lib/components'; import { imgTitle, tomorrowDate } from '@app/helpers'; import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; import { cn } from 'lib/utils'; @@ -32,7 +32,7 @@ export function CreateDailyPlanFormModal({ employeeId?: string; chooseMember?: boolean; }) { - const { handleSubmit, reset, register } = useForm(); + const { handleSubmit, reset } = useForm(); const { user } = useAuthenticateUser(); const { activeTeam, activeTeamManagers } = useOrganizationTeams(); const { createDailyPlan, createDailyPlanLoading } = useDailyPlan(); @@ -50,7 +50,7 @@ export function CreateDailyPlanFormModal({ async (values: any) => { const toDay = new Date(); createDailyPlan({ - workTimePlanned: parseInt(values.workTimePlanned), + workTimePlanned: parseInt(values.workTimePlanned) || 0, taskId, date: planMode == 'today' @@ -104,14 +104,14 @@ export function CreateDailyPlanFormModal({ /> )} - + /> */} {planMode === 'custom' && ( 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 c0ca5b92d..5a8aca31d 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -14,48 +14,47 @@ export function OutstandingAll({ profile }: OutstandingAll) { const { outstandingPlans } = useDailyPlan(); const view = useRecoilValue(dailyPlanViewHeaderTabs); const displayedTaskId = new Set(); + + const tasks = outstandingPlans.map((plan) => plan.tasks).reduce((red, curr) => red?.concat(curr || []), []); + return (
- {outstandingPlans?.length > 0 ? ( + {tasks && tasks?.length > 0 ? ( <> - {outstandingPlans?.map((plan) => ( - <> - {/* */} -
    - {plan?.tasks?.map((task) => { - //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' ? ( - - ) : ( - - ); - })} -
- - ))} + {/* */} +
    + {tasks?.map((task) => { + //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' ? ( + + ) : ( + + ); + })} +
) : ( diff --git a/apps/web/lib/features/task/task-block-card.tsx b/apps/web/lib/features/task/task-block-card.tsx index 3b6c7544b..30ab79b49 100644 --- a/apps/web/lib/features/task/task-block-card.tsx +++ b/apps/web/lib/features/task/task-block-card.tsx @@ -39,9 +39,9 @@ export default function TaskBlockCard(props: TaskItemProps) { const taskAssignee: ImageOverlapperProps[] = task.members?.map((member: any) => { return { - id: member.user.id, - url: member.user.imageUrl, - alt: member.user.firstName + id: member.user?.id, + url: member.user?.imageUrl, + alt: member.user?.firstName }; }); diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 192c035fd..6543e4ba1 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -132,9 +132,9 @@ export function TaskCard(props: Props) { const taskAssignee: ImageOverlapperProps[] = task?.members?.map((member: any) => { return { - id: member.user.id, - url: member.user.imageUrl, - alt: member.user.firstName + id: member.user?.id, + url: member.user?.imageUrl, + alt: member.user?.firstName }; }) || []; From da377da553d5d6429f439fdbf975dbb36e8413b9 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 16 Jul 2024 13:09:14 +0200 Subject: [PATCH 04/11] feat: add today plan notification trigger --- apps/web/app/constants.ts | 1 + apps/web/app/hooks/features/useDailyPlan.ts | 52 ++++++++++++++++--- .../task/daily-plan/outstanding-all.tsx | 2 +- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index cb2e49e5f..f6a52203e 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -266,6 +266,7 @@ export const languagesFlags = [ // Local storage keys export const LAST_WORSPACE_AND_TEAM = 'last-workspace-and-team'; export const USER_SAW_OUTSTANDING_NOTIFICATION = 'user-saw-notif'; +export const TODAY_PLAN_ALERT_SHOWN_DATE = 'last-today-plan-alert-date'; // OAuth providers keys diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index d60318923..edc87d10e 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -1,16 +1,16 @@ 'use client'; -import { useRecoilState } from 'recoil'; -import { useCallback, useEffect } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { useCallback, useEffect, useState } from 'react'; import { useQuery } from '../useQuery'; import { + activeTeamState, dailyPlanFetchingState, dailyPlanListState, employeePlansListState, myDailyPlanListState, profileDailyPlanListState, - taskPlans, - userState + taskPlans } from '@app/stores'; import { addTaskToPlanAPI, @@ -25,9 +25,22 @@ import { } from '@app/services/client/api'; import { ICreateDailyPlan, IDailyPlanTasksUpdate, IUpdateDailyPlan } from '@app/interfaces'; import { useFirstLoad } from '../useFirstLoad'; +import { useAuthenticateUser } from './useAuthenticateUser'; +import { TODAY_PLAN_ALERT_SHOWN_DATE } from '@app/constants'; + +type TodayPlanNotificationParams = { + canBeSeen: boolean; + alreadySeen: boolean; +}; export function useDailyPlan() { - const [user] = useRecoilState(userState); + const [addTodayPlanTrigger, setAddTodayPlanTrigger] = useState({ + canBeSeen: false, + alreadySeen: false + }); + + const { user } = useAuthenticateUser(); + const activeTeam = useRecoilValue(activeTeamState); const { loading, queryCall } = useQuery(getDayPlansByEmployeeAPI); const { loading: getAllDayPlansLoading, queryCall: getAllQueryCall } = useQuery(getAllDayPlansAPI); @@ -241,10 +254,34 @@ export function useDailyPlan() { profileDailyPlans.items && [...profileDailyPlans.items].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + const currentUser = activeTeam?.members?.find((member) => member.employee.userId === user?.id); + useEffect(() => { getMyDailyPlans(); }, [getMyDailyPlans]); + useEffect(() => { + const checkAndShowAlert = () => { + if (activeTeam && currentUser) { + const lastAlertDate = localStorage.getItem(TODAY_PLAN_ALERT_SHOWN_DATE); + const today = new Date().toISOString().split('T')[0]; + const totalMemberWorked = currentUser?.totalTodayTasks.reduce( + (previousValue, currentValue) => previousValue + currentValue.duration, + 0 + ); + const showTodayPlanTrigger = todayPlan && todayPlan.length > 0 && totalMemberWorked > 0; + if (lastAlertDate === today) { + setAddTodayPlanTrigger({ canBeSeen: !!showTodayPlanTrigger, alreadySeen: true }); + } + } + }; + + checkAndShowAlert(); + const intervalId = setInterval(checkAndShowAlert, 24 * 60 * 60 * 1000); // One day check and display + + return () => clearInterval(intervalId); + }, [activeTeam, currentUser, todayPlan]); + return { dailyPlan, setDailyPlan, @@ -292,6 +329,9 @@ export function useDailyPlan() { pastPlans, outstandingPlans, todayPlan, - sortedPlans + sortedPlans, + + addTodayPlanTrigger, + setAddTodayPlanTrigger }; } 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 5a8aca31d..998d7ff49 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -26,7 +26,7 @@ export function OutstandingAll({ profile }: OutstandingAll) {
    From 99854ce79beac239197d19335d35571199e8a2aa Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 17 Jul 2024 01:56:29 +0200 Subject: [PATCH 05/11] feat: soft Daily Plan triggers --- apps/web/app/hooks/features/useTimer.ts | 7 +- .../components/shared/timer/timer-card.tsx | 1 + .../plans-work-time-and-estimate.tsx | 169 ++++++++++++------ apps/web/lib/features/task/task-card.tsx | 1 + apps/web/lib/features/timer/timer.tsx | 1 + apps/web/locales/ar.json | 5 +- apps/web/locales/bg.json | 5 +- apps/web/locales/de.json | 5 +- apps/web/locales/en.json | 7 +- apps/web/locales/es.json | 5 +- apps/web/locales/fr.json | 5 +- apps/web/locales/he.json | 5 +- apps/web/locales/it.json | 5 +- apps/web/locales/nl.json | 5 +- apps/web/locales/pl.json | 5 +- apps/web/locales/pt.json | 5 +- apps/web/locales/ru.json | 5 +- apps/web/locales/zh.json | 5 +- 18 files changed, 159 insertions(+), 87 deletions(-) diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts index 432a452c1..5599f114e 100644 --- a/apps/web/app/hooks/features/useTimer.ts +++ b/apps/web/app/hooks/features/useTimer.ts @@ -205,11 +205,8 @@ export function useTimer() { // If require plan setting is activated, // check if the today plan has working time planned and all the tasks into the plan are estimated - let isPlanVerified = true; - if (requirePlan) { - isPlanVerified = - !!hasPlan?.workTimePlanned && !!hasPlan?.tasks?.every((task) => task.estimate && task.estimate > 0); - } + const isPlanVerified = + !!hasPlan?.workTimePlanned && !!hasPlan?.tasks?.every((task) => task.estimate && task.estimate > 0); const canRunTimer = user?.isEmailVerified && diff --git a/apps/web/components/shared/timer/timer-card.tsx b/apps/web/components/shared/timer/timer-card.tsx index 3511902db..10f2a9d08 100644 --- a/apps/web/components/shared/timer/timer-card.tsx +++ b/apps/web/components/shared/timer/timer-card.tsx @@ -60,6 +60,7 @@ const Timer = () => { open={isOpen} plan={hasPlan} startTimer={startTimer} + hasPlan={!!hasPlan} /> ); diff --git a/apps/web/lib/features/daily-plan/plans-work-time-and-estimate.tsx b/apps/web/lib/features/daily-plan/plans-work-time-and-estimate.tsx index 885d0698a..f2c4d1dd0 100644 --- a/apps/web/lib/features/daily-plan/plans-work-time-and-estimate.tsx +++ b/apps/web/lib/features/daily-plan/plans-work-time-and-estimate.tsx @@ -1,24 +1,28 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { PiWarningCircleFill } from 'react-icons/pi'; -import { IDailyPlan, ITeamTask } from '@app/interfaces'; +import { DailyPlanStatusEnum, IDailyPlan, ITeamTask } from '@app/interfaces'; import { Card, InputField, Modal, Text, VerticalSeparator } from 'lib/components'; import { useTranslations } from 'use-intl'; import { TaskNameInfoDisplay } from '../task/task-displays'; import { Button } from '@components/ui/button'; import { TaskEstimate } from '../task/task-estimate'; -import { useDailyPlan, useTeamTasks } from '@app/hooks'; +import { useAuthenticateUser, useDailyPlan, useTeamTasks } from '@app/hooks'; +import { ReloadIcon } from '@radix-ui/react-icons'; export function AddWorkTimeAndEstimatesToPlan({ open, closeModal, plan, - startTimer + startTimer, + hasPlan // employee }: { open: boolean; closeModal: () => void; startTimer: () => void; + hasPlan: boolean; plan?: IDailyPlan; + // employee?: OT_Member; }) { const t = useTranslations(); @@ -30,15 +34,18 @@ export function AddWorkTimeAndEstimatesToPlan({ const { updateDailyPlan } = useDailyPlan(); - const { tasks: $tasks } = useTeamTasks(); + const { tasks: $tasks, activeTeam } = useTeamTasks(); const tasks = $tasks.filter((task) => plan?.tasks?.some((t) => task?.id === t.id && typeof task?.estimate === 'number' && task?.estimate <= 0) ); const handleSubmit = () => { - if (workTimePlanned === 0 || typeof workTimePlanned !== 'number') return; - if (tasks.some((task) => task.estimate === 0)) return; + const requirePlan = activeTeam?.requirePlanToTrack; + if (requirePlan) { + if (workTimePlanned === 0 || typeof workTimePlanned !== 'number') return; + if (tasks.some((task) => task.estimate === 0)) return; + } updateDailyPlan({ workTimePlanned }, plan?.id ?? ''); startTimer(); @@ -47,59 +54,63 @@ export function AddWorkTimeAndEstimatesToPlan({ return ( - -
    -
    - - {t('timer.todayPlanSettings.TITLE')} - -
    -
    - - {t('timer.todayPlanSettings.WORK_TIME_PLANNED')} * - - setworkTimePlanned(parseFloat(e.target.value))} - required - defaultValue={plan?.workTimePlanned ?? 0} - /> -
    + {hasPlan ? ( + +
    +
    + + {t('timer.todayPlanSettings.TITLE')} + +
    +
    + + {t('timer.todayPlanSettings.WORK_TIME_PLANNED')} * + + setworkTimePlanned(parseFloat(e.target.value))} + required + defaultValue={plan?.workTimePlanned ?? 0} + /> +
    - {tasks.length > 0 && ( -
    - + {tasks.length > 0 && ( +
    + -
    - -

    {t('timer.todayPlanSettings.WARNING_PLAN_ESTIMATION')}

    +
    + +

    {t('timer.todayPlanSettings.WARNING_PLAN_ESTIMATION')}

    +
    + )} + +
    + +
    - )} - -
    - -
    -
    - + + ) : ( + + )} ); } @@ -146,3 +157,51 @@ export function UnEstimatedTask({ task }: { task: ITeamTask }) { ); } + +export function CreateTodayPlanPopup({ closeModal }: { closeModal: () => void }) { + const { createDailyPlan, createDailyPlanLoading } = useDailyPlan(); + const { user } = useAuthenticateUser(); + const { activeTeam } = useTeamTasks(); + const member = activeTeam?.members.find((member) => member.employee.userId === user?.id); + const onSubmit = useCallback( + async (values: any) => { + const toDay = new Date(); + createDailyPlan({ + workTimePlanned: parseInt(values.workTimePlanned) || 0, + date: toDay, + status: DailyPlanStatusEnum.OPEN, + tenantId: user?.tenantId ?? '', + employeeId: member?.employeeId, + organizationId: member?.organizationId + }).then(() => { + closeModal(); + }); + }, + [closeModal, createDailyPlan, member?.employeeId, member?.organizationId, user?.tenantId] + ); + + return ( + +
    +
    + + CREATE A PLAN FOR TODAY + + + You are creating a new plan for today +
    +
    + +
    +
    +
    + ); +} diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 6543e4ba1..9b657343b 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -416,6 +416,7 @@ function TimerButtonCall({ open={isOpen} plan={hasPlan} startTimer={startTimer} + hasPlan={!!hasPlan} /> ); diff --git a/apps/web/lib/features/timer/timer.tsx b/apps/web/lib/features/timer/timer.tsx index d6245f893..fce5dc2ce 100644 --- a/apps/web/lib/features/timer/timer.tsx +++ b/apps/web/lib/features/timer/timer.tsx @@ -154,6 +154,7 @@ export function Timer({ className }: IClassName) { open={isOpen} plan={hasPlan} startTimer={startTimer} + hasPlan={!!hasPlan} />
    diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index 17e85f377..2b7ff1272 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -197,7 +197,8 @@ "NOT_WORKING": "غير عامل", "WORKING": "يعمل", "PAUSED": "متوقف", - "ONLINE": "متصل" + "ONLINE": "متصل", + "SKIP_ADD_LATER": "أضف لاحقًا" }, "hotkeys": { "HELP": "مساعدة", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "لديك", "USER_LABEL": "مهام غير مكتملة، يرجى التحقق منها", - "OUTSTANDING_VIEW": "عرض المهام المعلقة", + "OUTSTANDING_VIEW": "معلق", "VIEW_BUTTON": "عرض" } }, diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 3b43adbba..2186a167a 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Не работи", "WORKING": "Работи", "PAUSED": "Пауза", - "ONLINE": "Онлайн" + "ONLINE": "Онлайн", + "SKIP_ADD_LATER": "Добави по-късно" }, "hotkeys": { "HELP": "Помощ", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Имате", "USER_LABEL": "незавършени задачи, моля, проверете", - "OUTSTANDING_VIEW": "Преглед на изчакващите задачи", + "OUTSTANDING_VIEW": "Изключителен", "VIEW_BUTTON": "Преглед" } }, diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index 1ce0d9d1e..ca856b96d 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Nicht arbeiten", "WORKING": "Arbeiten", "PAUSED": "Pausiert", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Später hinzufügen" }, "hotkeys": { "HELP": "Hilfe", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Sie haben", "USER_LABEL": "unvollständige Aufgaben, bitte überprüfen", - "OUTSTANDING_VIEW": "Ausstehende Ansicht", + "OUTSTANDING_VIEW": "Hervorragend", "VIEW_BUTTON": "Ansehen" } }, diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index 1dc28e54d..07d45d2fb 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Not Working", "WORKING": "Working", "PAUSED": "Paused", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Add later" }, "hotkeys": { "HELP": "Help", @@ -229,9 +230,9 @@ "CONFIRM_ACCEPT_INVITATION": "Are you sure you want to accept the invitation?", "CONFIRM_REJECT_INVITATION": "Are you sure you want to reject the invitation?", "OUTSTANDING_NOTIFICATIONS": { - "SUBJECT": "You've", + "SUBJECT": "You have", "USER_LABEL": "uncompleted tasks, please check in", - "OUTSTANDING_VIEW": "Outstanding View", + "OUTSTANDING_VIEW": "Outstanding", "VIEW_BUTTON": "View" } }, diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 257403ef4..818ac74d5 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -197,7 +197,8 @@ "NOT_WORKING": "No trabajando", "WORKING": "Trabajando", "PAUSED": "Pausado", - "ONLINE": "En línea" + "ONLINE": "En línea", + "SKIP_ADD_LATER": "Agregar más tarde" }, "hotkeys": { "HELP": "Ayuda", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Tienes", "USER_LABEL": "tareas incompletas, por favor revisa", - "OUTSTANDING_VIEW": "Vista pendiente", + "OUTSTANDING_VIEW": "Pendiente", "VIEW_BUTTON": "Ver" } }, diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index 27c226ab1..6cd5044e2 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Non actif", "WORKING": "Actif", "PAUSED": "En pause", - "ONLINE": "En ligne" + "ONLINE": "En ligne", + "SKIP_ADD_LATER": "Ajouter plus tard" }, "hotkeys": { "HELP": "Aide", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Vous avez", "USER_LABEL": "des tâches incomplètes, veuillez vérifier", - "OUTSTANDING_VIEW": "Vue des tâches en attente", + "OUTSTANDING_VIEW": "En attente", "VIEW_BUTTON": "Voir" } }, diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 17de57399..d89cfbb2d 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -197,7 +197,8 @@ "NOT_WORKING": "לא עובד", "WORKING": "עובד", "PAUSED": "מושהה", - "ONLINE": "מחובר" + "ONLINE": "מחובר", + "SKIP_ADD_LATER": "הוסף מאוחר יותר" }, "hotkeys": { "HELP": "עזרה", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "יש לך", "USER_LABEL": "משימות שלא הושלמו, נא לבדוק", - "OUTSTANDING_VIEW": "תצוגה ממתינה", + "OUTSTANDING_VIEW": "יוצא דופן", "VIEW_BUTTON": "הצג" } }, diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 93262916f..3c7ff1fc9 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Non Lavora", "WORKING": "Lavora", "PAUSED": "In Pausa", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Aggiungi dopo" }, "hotkeys": { "HELP": "Aiuto", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Hai", "USER_LABEL": "compiti incompleti, controlla per favore", - "OUTSTANDING_VIEW": "Vista delle attività in sospeso", + "OUTSTANDING_VIEW": "Eccezionale", "VIEW_BUTTON": "Vedi" } }, diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index fad47a7c3..f6b88f6da 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Niet aan het werk", "WORKING": "Aan het werk", "PAUSED": "Gepauzeerd", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Later toevoegen" }, "hotkeys": { "HELP": "Help", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Je hebt", "USER_LABEL": "onvoltooide taken, controleer alsjeblieft", - "OUTSTANDING_VIEW": "Uitzicht op uitstaande taken", + "OUTSTANDING_VIEW": "Uitstekend", "VIEW_BUTTON": "Bekijk" } }, diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 1ec6b4080..21ec208c2 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Niepracujący", "WORKING": "Pracujący", "PAUSED": "Wstrzymany", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Dodaj później" }, "hotkeys": { "HELP": "Pomoc", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Masz", "USER_LABEL": "niezakończone zadania, proszę sprawdź", - "OUTSTANDING_VIEW": "Widok zadań do wykonania", + "OUTSTANDING_VIEW": "Wyjątkowy", "VIEW_BUTTON": "Zobacz" } }, diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index fef612fbe..3df8aeb73 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Não Trabalhando", "WORKING": "Trabalhando", "PAUSED": "Pausado", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Adicionar depois" }, "hotkeys": { "HELP": "Ajuda", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Você tem", "USER_LABEL": "tarefas não concluídas, por favor verifique", - "OUTSTANDING_VIEW": "Visualização de pendências", + "OUTSTANDING_VIEW": "Pendências", "VIEW_BUTTON": "Ver" } }, diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 51c727994..003e3c5fc 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Не работает", "WORKING": "Работает", "PAUSED": "Приостановлено", - "ONLINE": "Онлайн" + "ONLINE": "Онлайн", + "SKIP_ADD_LATER": "Добавить позже" }, "hotkeys": { "HELP": "Помощь", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "У вас есть", "USER_LABEL": "незавершенные задачи, пожалуйста, проверьте", - "OUTSTANDING_VIEW": "Просмотр незавершенных задач", + "OUTSTANDING_VIEW": "Выдающийся", "VIEW_BUTTON": "Просмотр" } }, diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index 64d4ea750..fe4fd5a16 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -197,7 +197,8 @@ "NOT_WORKING": "不工作", "WORKING": "工作", "PAUSED": "暂停", - "ONLINE": "在线" + "ONLINE": "在线", + "SKIP_ADD_LATER": "稍后添加" }, "hotkeys": { "HELP": "帮助", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "您有", "USER_LABEL": "未完成的任务,请检查", - "OUTSTANDING_VIEW": "待处理视图", + "OUTSTANDING_VIEW": "突出", "VIEW_BUTTON": "查看" } }, From e9eb607f7ae638248e5ae9d194ac7b85f408530b Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 17 Jul 2024 10:32:04 +0200 Subject: [PATCH 06/11] refact: daily plan badge --- apps/web/app/helpers/plan-day-badge.ts | 10 +++-- apps/web/lib/components/kanban-card.tsx | 8 +++- .../user-team-active-task.tsx | 1 + .../features/task/daily-plan/past-tasks.tsx | 5 ++- .../features/task/task-all-status-type.tsx | 20 ++++++++-- .../web/lib/features/task/task-block-card.tsx | 8 +++- apps/web/lib/features/task/task-card.tsx | 12 ++++-- apps/web/lib/features/task/task-filters.tsx | 17 ++++---- .../team/user-team-block/task-info.tsx | 39 +++++++++++++++---- .../features/team/user-team-card/index.tsx | 15 +++++-- .../team/user-team-card/task-info.tsx | 18 +++++++-- apps/web/lib/features/user-profile-plans.tsx | 9 ++--- 12 files changed, 118 insertions(+), 44 deletions(-) diff --git a/apps/web/app/helpers/plan-day-badge.ts b/apps/web/app/helpers/plan-day-badge.ts index e5d9322da..70c1f92ed 100644 --- a/apps/web/app/helpers/plan-day-badge.ts +++ b/apps/web/app/helpers/plan-day-badge.ts @@ -1,7 +1,11 @@ import { IDailyPlan, ITeamTask } from '@app/interfaces'; import { formatDayPlanDate } from './date'; -export const planBadgeContent = (plans: IDailyPlan[], taskId: ITeamTask['id']): string | null => { +export const planBadgeContent = ( + plans: IDailyPlan[], + taskId: ITeamTask['id'], + tab?: 'default' | 'unassign' | 'dailyplan' +): string | null => { // Search a plan that contains a given task const plan = plans.find((plan) => plan.tasks?.some((task) => task.id === taskId)); @@ -12,8 +16,8 @@ export const planBadgeContent = (plans: IDailyPlan[], taskId: ITeamTask['id']): (pl) => pl.id !== plan.id && pl.tasks?.some((tsk) => tsk.id === taskId) ); - // If the task exists in other plans, the its planned many days - if (otherPlansWithTask.length > 0) { + // If the task exists in other plans, then its planned many days + if (otherPlansWithTask.length > 0 || tab === 'unassign') { return 'Planned'; } else { return `${formatDayPlanDate(plan.date, 'DD MMM YYYY')}`; diff --git a/apps/web/lib/components/kanban-card.tsx b/apps/web/lib/components/kanban-card.tsx index 98b38a2d9..e97005508 100644 --- a/apps/web/lib/components/kanban-card.tsx +++ b/apps/web/lib/components/kanban-card.tsx @@ -174,7 +174,13 @@ export default function Item(props: ItemProps) {
    - + diff --git a/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx b/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx index ded3e360e..27a75ce65 100644 --- a/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx +++ b/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx @@ -26,6 +26,7 @@ export default function UserTeamActiveTaskInfo({ member }: { member: OT_Member } memberInfo={memberInfo} className="flex-1 lg:px-4 px-2 overflow-y-hidden" publicTeam={false} + tab="default" /> ) : (
    --
    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 4055784de..683cb467f 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -1,6 +1,6 @@ import { formatDayPlanDate, yesterdayDate } from '@app/helpers'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; -import { EmptyPlans, PlanHeader } from 'lib/features/user-profile-plans'; +import { EmptyPlans, FilterTabs, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; import { useDailyPlan } from '@app/hooks'; import { useRecoilValue } from 'recoil'; @@ -10,7 +10,7 @@ import { clsxm } from '@app/utils'; import TaskBlockCard from '../task-block-card'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; -export function PastTasks({ profile }: { profile: any }) { +export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any; currentTab?: FilterTabs }) { const { pastPlans } = useDailyPlan(); const view = useRecoilValue(dailyPlanViewHeaderTabs); @@ -63,6 +63,7 @@ export function PastTasks({ profile }: { profile: any }) { type="HORIZONTAL" taskBadgeClassName={`rounded-sm`} taskTitleClassName="mt-[0.0625rem]" + planMode={currentTab === 'Past Tasks' ? 'Past Tasks' : undefined} /> ) : ( diff --git a/apps/web/lib/features/task/task-all-status-type.tsx b/apps/web/lib/features/task/task-all-status-type.tsx index ce783a704..f4224ffd2 100644 --- a/apps/web/lib/features/task/task-all-status-type.tsx +++ b/apps/web/lib/features/task/task-all-status-type.tsx @@ -14,13 +14,18 @@ import { import { clsxm } from '@app/utils'; import { planBadgeContent } from '@app/helpers'; import { CalendarIcon } from '@radix-ui/react-icons'; +import { FilterTabs } from '../user-profile-plans'; export function TaskAllStatusTypes({ task, showStatus = false, toBlockCard = false, - className + className, + tab, + dayPlanTab }: { + tab?: 'default' | 'unassign' | 'dailyplan'; + dayPlanTab?: FilterTabs; task?: Nullable; showStatus?: boolean; toBlockCard?: boolean; @@ -94,10 +99,17 @@ export function TaskAllStatusTypes({ titleClassName={'text-[0.625rem] font-[500]'} /> )} - {planBadgeContent(dailyPlan.items, task?.id ?? '') && ( -
    + {planBadgeContent(dailyPlan.items, task?.id ?? '', tab) && ( +
    - {planBadgeContent(dailyPlan.items, task?.id ?? '')} + + {planBadgeContent(dailyPlan.items, task?.id ?? '', tab)} +
    )} {tags.map((tag, i) => { diff --git a/apps/web/lib/features/task/task-block-card.tsx b/apps/web/lib/features/task/task-block-card.tsx index 30ab79b49..7ba6f64f3 100644 --- a/apps/web/lib/features/task/task-block-card.tsx +++ b/apps/web/lib/features/task/task-block-card.tsx @@ -63,7 +63,13 @@ export default function TaskBlockCard(props: TaskItemProps) {
    - + diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 6543e4ba1..5d88b0043 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -160,6 +160,8 @@ export function TaskCard(props: Props) { className="px-4 w-full" taskBadgeClassName={clsxm(taskBadgeClassName)} taskTitleClassName={clsxm(taskTitleClassName)} + dayPlanTab={planMode} + tab={viewType} />
    @@ -248,7 +250,7 @@ export function TaskCard(props: Props) { )} */}
    - {' '} + {' '} {viewType === 'default' && ( <>
    @@ -427,8 +429,12 @@ function TaskInfo({ className, task, taskBadgeClassName, - taskTitleClassName + taskTitleClassName, + tab, + dayPlanTab }: IClassName & { + tab: 'default' | 'unassign' | 'dailyplan'; + dayPlanTab?: FilterTabs; task?: Nullable; taskBadgeClassName?: string; taskTitleClassName?: string; @@ -457,7 +463,7 @@ function TaskInfo({ )} {/* Task status */} - {task && } + {task && } {!task &&
    --
    }
    ); diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index d8cff29ce..c98e43020 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -23,7 +23,7 @@ import { DailyPlanFilter } from './daily-plan/daily-plan-filter'; import { useDateRange } from '@app/hooks/useDateRange'; import { TaskDatePickerWithRange } from './task-date-range'; -type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; +export type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; type ITabs = { tab: ITab; name: string; @@ -176,9 +176,9 @@ export function useTaskFilter(profile: I_UserProfilePage) { .every((k) => { return k === 'label' ? intersection( - statusFilters[k], - task['tags'].map((item) => item.name) - ).length === statusFilters[k].length + statusFilters[k], + task['tags'].map((item) => item.name) + ).length === statusFilters[k].length : statusFilters[k].includes(task[k]); }); }); @@ -213,7 +213,6 @@ export type I_TaskFilter = ReturnType; type Props = { hook: I_TaskFilter; profile: I_UserProfilePage }; export function TaskFilter({ className, hook, profile }: IClassName & Props) { - return (
    { - setDailyPlanTab(window.localStorage.getItem('daily-plan-tab') || "Future Tasks") - }, [dailyPlanTab]) + setDailyPlanTab(window.localStorage.getItem('daily-plan-tab') || 'Future Tasks'); + }, [dailyPlanTab]); return (
    @@ -415,7 +412,7 @@ export function TaskStatusFilter({ hook, employeeId }: { hook: I_TaskFilter; emp {hook.tab === 'dailyplan' && } {['Future Tasks', 'Past Tasks', 'All Tasks'].includes(dailyPlanTab) && ( - setDate(range)} label='Planned date' /> + setDate(range)} label="Planned date" /> )} diff --git a/apps/web/lib/features/team/user-team-block/task-info.tsx b/apps/web/lib/features/team/user-team-block/task-info.tsx index b92fd4543..93ab87cdb 100644 --- a/apps/web/lib/features/team/user-team-block/task-info.tsx +++ b/apps/web/lib/features/team/user-team-block/task-info.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import { I_TeamMemberCardHook, I_TMCardTaskEditHook } from '@app/hooks'; import { IClassName } from '@app/interfaces'; import { clsxm } from '@app/utils'; -import { TaskAllStatusTypes, TaskInput, TaskNameInfoDisplay } from 'lib/features'; +import { FilterTabs, TaskAllStatusTypes, TaskInput, TaskNameInfoDisplay } from 'lib/features'; import { useRouter } from 'next/navigation'; import { useTranslations } from 'next-intl'; @@ -10,33 +9,49 @@ type Props = IClassName & { edition: I_TMCardTaskEditHook; memberInfo: I_TeamMemberCardHook; publicTeam?: boolean; + tab?: 'default' | 'unassign' | 'dailyplan'; + dayPlanTab?: FilterTabs; }; -export function TaskInfo({ className, memberInfo, edition, publicTeam }: Props) { +export function TaskInfo({ className, memberInfo, edition, publicTeam, tab, dayPlanTab }: Props) { return (
    {/* task */}
    {edition.task && ( - + )} {!edition.task &&
    --
    }
    - {edition.task && } + {edition.task && ( + + )} {!edition.task &&
    --
    }
    ); } -export function TaskBlockInfo({ className, memberInfo, edition, publicTeam }: Props) { +export function TaskBlockInfo({ className, memberInfo, edition, publicTeam, tab, dayPlanTab }: Props) { const t = useTranslations(); return (
    {/* task */}
    {edition.task && ( - + )} {!edition.task && (
    @@ -45,7 +60,15 @@ export function TaskBlockInfo({ className, memberInfo, edition, publicTeam }: Pr )}
    - {edition.task && } + {edition.task && ( + + )} {!edition.task &&
    _
    }
    ); diff --git a/apps/web/lib/features/team/user-team-card/index.tsx b/apps/web/lib/features/team/user-team-card/index.tsx index 5dbcb5f38..88bf8b117 100644 --- a/apps/web/lib/features/team/user-team-card/index.tsx +++ b/apps/web/lib/features/team/user-team-card/index.tsx @@ -195,13 +195,15 @@ export function UserTeamCard({ memberInfo={memberInfo} className="flex-1 lg:px-4 px-2 overflow-y-hidden" publicTeam={publicTeam} + tab="default" /> {isManagerConnectedUser != 1 ? (

    { - showActivityFilter('TICKET', memberInfo.member ?? null); setUserDetailAccordion(''); + showActivityFilter('TICKET', memberInfo.member ?? null); + setUserDetailAccordion(''); }} > {!showActivity ? ( @@ -254,7 +256,8 @@ export function UserTeamCard({

    {menu}
    {userDetailAccordion == memberInfo.memberUser?.id && - memberInfo.memberUser.id == profile.userProfile?.id && !showActivity? ( + memberInfo.memberUser.id == profile.userProfile?.id && + !showActivity ? (
    {canSeeActivity && ( @@ -300,7 +303,13 @@ export function UserTeamCard({
    - +
    diff --git a/apps/web/lib/features/team/user-team-card/task-info.tsx b/apps/web/lib/features/team/user-team-card/task-info.tsx index b96965fe4..36b291c9d 100644 --- a/apps/web/lib/features/team/user-team-card/task-info.tsx +++ b/apps/web/lib/features/team/user-team-card/task-info.tsx @@ -1,16 +1,18 @@ import { I_TeamMemberCardHook, I_TMCardTaskEditHook } from '@app/hooks'; import { IClassName } from '@app/interfaces'; import { clsxm } from '@app/utils'; -import { TaskAllStatusTypes, TaskInput, TaskNameInfoDisplay } from 'lib/features'; +import { FilterTabs, TaskAllStatusTypes, TaskInput, TaskNameInfoDisplay } from 'lib/features'; import { useRouter } from 'next/navigation'; type Props = IClassName & { edition: I_TMCardTaskEditHook; memberInfo: I_TeamMemberCardHook; publicTeam?: boolean; + dayPlanTab?: FilterTabs; + tab?: 'default' | 'unassign' | 'dailyplan'; }; -export function TaskInfo({ className, memberInfo, edition, publicTeam }: Props) { +export function TaskInfo({ className, memberInfo, edition, publicTeam, tab, dayPlanTab }: Props) { return ( <> {!edition.task &&
    --
    } @@ -30,11 +32,19 @@ export function TaskInfo({ className, memberInfo, edition, publicTeam }: Props) )} > {edition.task && ( - + )}
    - {edition.task && } + {edition.task && ( + + )}
    {!edition.task &&
    --
    } diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index 1a0bccedd..b23168c25 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -22,7 +22,7 @@ import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; import TaskBlockCard from './task/task-block-card'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; -type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; +export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; type FilterOutstanding = 'ALL' | 'DATE'; export function UserProfilePlans() { @@ -142,7 +142,7 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current let filteredPlans: IDailyPlan[] = []; const { deleteDailyPlan, deleteDailyPlanLoading, sortedPlans, todayPlan } = useDailyPlan(); const [popupOpen, setPopupOpen] = useState(false); - const [currentId, setCurrentId] = useState(""); + const [currentId, setCurrentId] = useState(''); filteredPlans = sortedPlans; if (currentTab === 'Today Tasks') filteredPlans = todayPlan; @@ -153,7 +153,6 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current const view = useRecoilValue(dailyPlanViewHeaderTabs); - return (
    {filterPlans?.length > 0 ? ( @@ -225,8 +224,8 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current //button open popup + } > {/*button confirm*/} diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index a5f070681..1db976457 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -24,6 +24,7 @@ import { IDailyPlanMode, IDailyPlanTasksUpdate, IOrganizationTeamList, + IRemoveTaskFromManyPlans, ITeamTask, Nullable, OT_Member @@ -598,6 +599,12 @@ function TaskCardMenu({ plan={plan} />
    +
    + +
    ) : ( <> @@ -755,3 +762,23 @@ export function RemoveTaskFromPlan({ task, plan, member }: { task: ITeamTask; me ); } + +export function RemoveManyTaskFromPlan({ task, member }: { task: ITeamTask; member?: OT_Member; }) { + // const t = useTranslations(); + const { removeManyTaskPlans } = useDailyPlan(); + const data: IRemoveTaskFromManyPlans = { plansIds: [], employeeId: member?.employeeId }; + const onClick = () => { + removeManyTaskPlans(data, task.id ?? ''); + }; + return ( + + Remove from all plans + + ); +} diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index b23168c25..ac16b05e7 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -142,7 +142,7 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current let filteredPlans: IDailyPlan[] = []; const { deleteDailyPlan, deleteDailyPlanLoading, sortedPlans, todayPlan } = useDailyPlan(); const [popupOpen, setPopupOpen] = useState(false); - const [currentId, setCurrentId] = useState(''); + const [currentDeleteIndex, setCurrentDeleteIndex] = useState(0); filteredPlans = sortedPlans; if (currentTab === 'Today Tasks') filteredPlans = todayPlan; @@ -150,7 +150,6 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current const canSeeActivity = useCanSeeActivityScreen(); const { filteredAllPlanData: filterAllPlanData } = useFilterDateRange(filteredPlans, 'all'); const filterPlans: IDailyPlan[] = currentTab === 'All Tasks' ? filterAllPlanData : filteredPlans; - const view = useRecoilValue(dailyPlanViewHeaderTabs); return ( @@ -165,7 +164,7 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current : [filterPlans?.map((plan) => new Date(plan.date).toISOString().split('T')[0])[0]] } > - {filterPlans?.map((plan) => ( + {filterPlans?.map((plan, index) => ( { - setCurrentId(plan.id ?? ''); - setPopupOpen(true); + setCurrentDeleteIndex(index) + setPopupOpen(prev => !prev); + }} 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" From 07ca32d949ff62bb3f347318667000cb5ea3158e Mon Sep 17 00:00:00 2001 From: "Gloire Mutaliko (Salva)" <86450367+GloireMutaliko21@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:37:40 +0200 Subject: [PATCH 08/11] [Fix] Profile | tab 'Assigned' values in Action menu (#2745) * feat: hide plan for today and for tomorrow on already existing plans * feat: dailyn plan creation disable dates already planned * feat: dailyn plan creation disable dates using profile user plans * fix: delete unncessary logs --- apps/web/app/hooks/features/useTimer.ts | 8 ++++++++ .../daily-plan/create-daily-plan-form-modal.tsx | 14 ++++++++++---- apps/web/lib/features/task/task-card.tsx | 17 ++++++++++++----- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts index 5599f114e..6f1b28515 100644 --- a/apps/web/app/hooks/features/useTimer.ts +++ b/apps/web/app/hooks/features/useTimer.ts @@ -193,6 +193,11 @@ export function useTimer() { plan.tasks?.length > 0 ); + const tomorrow = moment().add(1, 'days'); + const hasPlanForTomorrow = myDailyPlans.items.find( + (plan) => moment(plan.date).format('YYYY-MM-DD') === tomorrow.format('YYYY-MM-DD') + ); + // Team setting that tells if each member must have a today plan for allowing tracking time const requirePlan = activeTeam?.requirePlanToTrack; @@ -420,6 +425,7 @@ export function useTimer() { startTimer, stopTimer, hasPlan, + hasPlanForTomorrow, canRunTimer, canTrack, isPlanVerified, @@ -460,6 +466,7 @@ export function useTimerView() { startTimer, stopTimer, hasPlan, + hasPlanForTomorrow, canRunTimer, canTrack, isPlanVerified, @@ -491,6 +498,7 @@ export function useTimerView() { timerStatus, activeTeamTask, hasPlan, + hasPlanForTomorrow, disabled: !canRunTimer, canTrack, isPlanVerified, diff --git a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx index a60d3dfac..8491d203a 100644 --- a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx +++ b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx @@ -35,7 +35,9 @@ export function CreateDailyPlanFormModal({ const { handleSubmit, reset } = useForm(); const { user } = useAuthenticateUser(); const { activeTeam, activeTeamManagers } = useOrganizationTeams(); - const { createDailyPlan, createDailyPlanLoading } = useDailyPlan(); + const { createDailyPlan, createDailyPlanLoading, profileDailyPlans } = useDailyPlan(); + + const existingPlanDates = profileDailyPlans.items.map((plan) => new Date(plan.date)); const isManagerConnectedUser = activeTeamManagers.find((member) => member.employee?.user?.id == user?.id); @@ -119,7 +121,7 @@ export function CreateDailyPlanFormModal({ - + setDate(day ? day : new Date(tomorrowDate))} initialFocus - disabled={{ from: new Date(1970, 1, 1), to: tomorrowDate }} + disabled={[ + ...existingPlanDates, + { from: new Date(1970, 1, 1), to: tomorrowDate } + ]} /> diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 1db976457..64b03790d 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -500,6 +500,7 @@ function TaskCardMenu({ }, [memberInfo, task, viewType]); const canSeeActivity = useCanSeeActivityScreen(); + const { hasPlan, hasPlanForTomorrow } = useTimerView(); return ( @@ -556,6 +557,7 @@ function TaskCardMenu({ planMode="today" taskId={task.id} employeeId={profile?.member?.employeeId ?? ''} + hasTodayPlan={hasPlan} />
  • @@ -563,6 +565,7 @@ function TaskCardMenu({ planMode="tomorow" taskId={task.id} employeeId={profile?.member?.employeeId ?? ''} + hasPlanForTomorrow={hasPlanForTomorrow} />
  • @@ -599,7 +602,7 @@ function TaskCardMenu({ plan={plan} />
  • -
    +
    - {planMode === 'today' && ( + {planMode === 'today' && !hasTodayPlan && ( {isPending ? ( @@ -711,7 +718,7 @@ export function PlanTask({ )} )} - {planMode === 'tomorow' && ( + {planMode === 'tomorow' && !hasPlanForTomorrow && ( {isPending ? ( @@ -763,7 +770,7 @@ export function RemoveTaskFromPlan({ task, plan, member }: { task: ITeamTask; me ); } -export function RemoveManyTaskFromPlan({ task, member }: { task: ITeamTask; member?: OT_Member; }) { +export function RemoveManyTaskFromPlan({ task, member }: { task: ITeamTask; member?: OT_Member }) { // const t = useTranslations(); const { removeManyTaskPlans } = useDailyPlan(); const data: IRemoveTaskFromManyPlans = { plansIds: [], employeeId: member?.employeeId }; From ef94bf08e2096483a6b2046d6aeb267b06f8ee96 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:37:57 +0200 Subject: [PATCH 09/11] [Task] Daily Plan | Drag and Drop (#2743) * feat: remove many daily plans * feat:Implement task cards Drag & Drop (Daily Plan) * fix: cspelling error * fix: cspelling error * fix: merge conflicts * fix: error drag drop null check --------- Co-authored-by: GloireMutaliko21 --- apps/extensions/components/popup/Timer.tsx | 2 +- apps/web/app/helpers/drag-and-drop.ts | 48 ++++ apps/web/app/helpers/index.ts | 1 + apps/web/app/interfaces/IDailyPlan.ts | 8 +- .../features/task/daily-plan/future-tasks.tsx | 240 +++++++++------- .../task/daily-plan/outstanding-all.tsx | 112 +++++--- .../task/daily-plan/outstanding-date.tsx | 149 ++++++---- .../features/task/daily-plan/past-tasks.tsx | 160 +++++++---- apps/web/lib/features/task/task-filters.tsx | 1 - .../lib/features/team-members-kanban-view.tsx | 6 +- .../lib/features/team-members-table-view.tsx | 18 +- apps/web/lib/features/user-profile-plans.tsx | 271 ++++++++++-------- 12 files changed, 642 insertions(+), 374 deletions(-) create mode 100644 apps/web/app/helpers/drag-and-drop.ts diff --git a/apps/extensions/components/popup/Timer.tsx b/apps/extensions/components/popup/Timer.tsx index 57341bed9..bd6c08c1e 100644 --- a/apps/extensions/components/popup/Timer.tsx +++ b/apps/extensions/components/popup/Timer.tsx @@ -25,7 +25,7 @@ const Timer: React.FC = ({ port }) => { ? new Date(msg.payload.timer * 1000).toISOString().substr(11, 8) : '00:00:00'; const totalWorkedTime = - msg.payload.totalWorked > 0 + msg.payload!.totalWorked > 0 ? new Date(msg.payload.totalWorked * 1000).toISOString().substr(11, 8) : '00:00:00'; setTimeString(taskWorkedTime); diff --git a/apps/web/app/helpers/drag-and-drop.ts b/apps/web/app/helpers/drag-and-drop.ts new file mode 100644 index 000000000..c1d3af554 --- /dev/null +++ b/apps/web/app/helpers/drag-and-drop.ts @@ -0,0 +1,48 @@ +import { IDailyPlan, ITeamTask } from "@app/interfaces"; +import { DropResult } from "react-beautiful-dnd"; + +export const handleDragAndDrop = (results: DropResult, plans: IDailyPlan[], setPlans: React.Dispatch>) => { + const { source, destination } = results; + + if (!destination || (source.droppableId === destination.droppableId && source.index === destination.index)) return; + + const newPlans = [...plans]; + + const planSourceIndex = newPlans.findIndex(plan => plan.id === source.droppableId); + const planDestinationIndex = newPlans.findIndex(plan => plan.id === destination.droppableId); + + const newSourceTasks = [...newPlans[planSourceIndex].tasks!]; + const newDestinationTasks = source.droppableId !== destination.droppableId + ? [...newPlans[planDestinationIndex].tasks!] + : newSourceTasks; + + const [deletedTask] = newSourceTasks.splice(source.index, 1); + newDestinationTasks.splice(destination.index, 0, deletedTask); + + newPlans[planSourceIndex] = { + ...newPlans[planSourceIndex], + tasks: newSourceTasks, + }; + newPlans[planDestinationIndex] = { + ...newPlans[planDestinationIndex], + tasks: newDestinationTasks, + }; + setPlans(newPlans); +}; + + +export const handleDragAndDropDailyOutstandingAll = ( + results: DropResult, + tasks: ITeamTask[], + setTasks: React.Dispatch> +) => { + const { source, destination } = results; + + if (!destination || (source.droppableId === destination.droppableId && source.index === destination.index)) return; + + const newTasks = [...tasks]; + const [movedTask] = newTasks.splice(source.index, 1); + newTasks.splice(destination.index, 0, movedTask); + + setTasks(newTasks); +}; diff --git a/apps/web/app/helpers/index.ts b/apps/web/app/helpers/index.ts index 5ec7a1bb8..de35a0e57 100644 --- a/apps/web/app/helpers/index.ts +++ b/apps/web/app/helpers/index.ts @@ -9,3 +9,4 @@ export * from './validations'; export * from './colors'; export * from './strings'; export * from './plan-day-badge'; +export * from './drag-and-drop' diff --git a/apps/web/app/interfaces/IDailyPlan.ts b/apps/web/app/interfaces/IDailyPlan.ts index b14d60521..5d91cd89a 100644 --- a/apps/web/app/interfaces/IDailyPlan.ts +++ b/apps/web/app/interfaces/IDailyPlan.ts @@ -9,10 +9,10 @@ export interface IDailyPlanBase extends IBasePerTenantAndOrganizationEntity { status: DailyPlanStatusEnum; } -export class IRemoveTaskFromManyPlans { +export interface IRemoveTaskFromManyPlans { employeeId?: IEmployee['id']; plansIds?: IDailyPlan['id'][]; - organizationId?: IOrganization['id'] + organizationId?: IOrganization['id']; } export interface IDailyPlan extends IDailyPlanBase, IRelationnalEmployee { @@ -23,11 +23,11 @@ export interface ICreateDailyPlan extends IDailyPlanBase, IRelationnalEmployee { taskId?: ITeamTask['id']; } -export interface IUpdateDailyPlan extends Partial, Pick { } +export interface IUpdateDailyPlan extends Partial, Pick {} export interface IDailyPlanTasksUpdate extends Pick, - IBasePerTenantAndOrganizationEntity { } + IBasePerTenantAndOrganizationEntity {} export enum DailyPlanStatusEnum { OPEN = 'open', 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 fdf80c665..f813116ea 100644 --- a/apps/web/lib/features/task/daily-plan/future-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/future-tasks.tsx @@ -1,4 +1,4 @@ -import { formatDayPlanDate, tomorrowDate } from '@app/helpers'; +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'; @@ -13,6 +13,8 @@ import { HorizontalSeparator } from 'lib/components'; import { useState } from 'react'; import { AlertPopup } from 'lib/components'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; +import { IDailyPlan } from '@app/interfaces'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; export function FutureTasks({ profile }: { profile: any }) { const { deleteDailyPlan, deleteDailyPlanLoading, futurePlans } = useDailyPlan(); @@ -20,116 +22,150 @@ export function FutureTasks({ profile }: { profile: any }) { const [popupOpen, setPopupOpen] = useState(false); const { filteredFuturePlanData: filteredFuturePlanData } = useFilterDateRange(futurePlans, 'future'); const [currentDeleteIndex, setCurrentDeleteIndex] = useState(0); + const [futureDailyPlanTasks, setFutureDailyPlanTasks] = useState(filteredFuturePlanData); const view = useRecoilValue(dailyPlanViewHeaderTabs); return (
    - {filteredFuturePlanData.length > 0 ? ( - 0 ? ( + handleDragAndDrop(result, futureDailyPlanTasks, setFutureDailyPlanTasks)} > - {filteredFuturePlanData.map((plan, index) => ( - - -
    -
    - {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + + {futureDailyPlanTasks.map((plan, index) => ( + + +
    +
    + {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
    +
    - -
    - - - {/* Plan header */} - - - {/* Plan tasks list */} -
      - {plan.tasks?.map((task) => - view === 'CARDS' ? ( - - ) : ( - - ) - )} -
    - - {/* Delete Plan */} - {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*/} - - -
    - ) : ( - <> - )} -
    - - ))} - + {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 998d7ff49..80e56fd96 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -6,6 +6,10 @@ import { useRecoilValue } from 'recoil'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; import TaskBlockCard from '../task-block-card'; import { clsxm } from '@app/utils'; +import { DragDropContext, Draggable, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; +import { useState } from 'react'; +import { ITeamTask } from '@app/interfaces'; +import { handleDragAndDropDailyOutstandingAll } from '@app/helpers'; interface OutstandingAll { profile: any; @@ -16,45 +20,89 @@ export function OutstandingAll({ profile }: OutstandingAll) { const displayedTaskId = new Set(); const tasks = outstandingPlans.map((plan) => plan.tasks).reduce((red, curr) => red?.concat(curr || []), []); + const [task, setTask] = useState(tasks!); return (
+ {tasks && tasks?.length > 0 ? ( <> - {/* */} -
    handleDragAndDropDailyOutstandingAll(result, task, setTask)} > - {tasks?.map((task) => { - //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: DroppableProvided, snapshot: DroppableStateSnapshot) => ( +
    + {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 9eaa232ac..2623e624b 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx @@ -1,4 +1,4 @@ -import { formatDayPlanDate } from '@app/helpers'; +import { formatDayPlanDate, handleDragAndDrop } 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'; @@ -8,6 +8,9 @@ import TaskBlockCard from '../task-block-card'; import { clsxm } from '@app/utils'; import { useRecoilValue } from 'recoil'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; +import { useState } from 'react'; +import { IDailyPlan } from '@app/interfaces'; interface IOutstandingFilterDate { profile: any; @@ -15,64 +18,100 @@ interface IOutstandingFilterDate { export function OutstandingFilterDate({ profile }: IOutstandingFilterDate) { const { outstandingPlans } = useDailyPlan(); const view = useRecoilValue(dailyPlanViewHeaderTabs); + const [outstandingTasks, setOutstandingTasks] = useState(outstandingPlans) return (
- {outstandingPlans?.length > 0 ? ( - new Date(plan.date).toISOString().split('T')[0])} - > - {outstandingPlans?.map((plan) => ( - - -
-
- {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + {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 */} - + + + {/* Plan header */} + + + {(provided) => ( +
    + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
    + +
    + )} - {/* Plan tasks list */} -
      - {plan.tasks?.map((task) => - view === 'CARDS' ? ( - - ) : ( - - ) - )} -
    - - - ))} - +
    + ) : ( + + {(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 683cb467f..412ef7d05 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -1,4 +1,4 @@ -import { formatDayPlanDate, yesterdayDate } from '@app/helpers'; +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'; @@ -9,71 +9,119 @@ import { HorizontalSeparator } from 'lib/components'; import { clsxm } from '@app/utils'; import TaskBlockCard from '../task-block-card'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; +import { useState } from 'react'; +import { IDailyPlan } from '@app/interfaces'; +import { DragDropContext, Draggable, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any; currentTab?: FilterTabs }) { const { pastPlans } = useDailyPlan(); const view = useRecoilValue(dailyPlanViewHeaderTabs); const { filteredPastPlanData: filteredPastPlanData } = useFilterDateRange(pastPlans, 'past'); + const [pastTasks, setPastTasks] = useState(filteredPastPlanData); return (
- {filteredPastPlanData?.length > 0 ? ( - - {filteredPastPlanData?.map((plan) => ( - - -
-
- {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + {pastTasks?.length > 0 ? ( + handleDragAndDrop(result, pastPlans, setPastTasks)}> + + {pastTasks?.map((plan) => ( + + +
+
+ {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
+
- -
- - - {/* Plan header */} - - - {/* Plan tasks list */} -
    - {plan.tasks?.map((task) => - view === 'CARDS' ? ( - - ) : ( - - ) - )} -
-
- - ))} - + + + {/* Plan header */} + + + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( +
    + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
    + +
    + )} +
    + ) : ( + + {(provided) => ( +
    + +
    + )} +
    + ) + )} +
+ )} +
+
+ + ))} + + ) : ( )} diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index c98e43020..e2e1a8659 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -263,7 +263,6 @@ export function TaskFilter({ className, hook, profile }: IClassName & Props) { function InputFilters({ hook, profile }: Props) { const t = useTranslations(); const [loading, setLoading] = useState(false); - const osSpecificAssignTaskTooltipLabel = 'A'; return ( diff --git a/apps/web/lib/features/team-members-kanban-view.tsx b/apps/web/lib/features/team-members-kanban-view.tsx index 3433236d7..678966836 100644 --- a/apps/web/lib/features/team-members-kanban-view.tsx +++ b/apps/web/lib/features/team-members-kanban-view.tsx @@ -30,7 +30,7 @@ export const KanbanView = ({ kanbanBoardTasks, isLoading }: { kanbanBoardTasks: const [columns, setColumn] = useState( Object.keys(kanbanBoardTasks).map((key) => { const columnInfo = kanbanColumns.find((item) => item.name === key); - return { id: columnInfo?.id, name: key, icon: columnInfo ? columnInfo.fullIconUrl : '',color: columnInfo?.color }; + return { id: columnInfo?.id, name: key, icon: columnInfo ? columnInfo.fullIconUrl : '', color: columnInfo?.color }; }) ); const fullWidth = useRecoilValue(fullWidthState); @@ -198,7 +198,9 @@ export const KanbanView = ({ kanbanBoardTasks, isLoading }: { kanbanBoardTasks: <> {Array.isArray(columns) && columns.length > 0 && ( - + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
- []} - data={sortedTeamMembers} - noResultsMessage={{ - heading: 'No team members found', - content: 'Try adjusting your search or filter to find what you’re looking for.' - }} - /> + []} + data={sortedTeamMembers} + noResultsMessage={{ + heading: 'No team members found', + content: 'Try adjusting your search or filter to find what you’re looking for.' + }} + /> ); diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index ac16b05e7..6109c8810 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -1,5 +1,4 @@ 'use client'; - import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { useCanSeeActivityScreen, useDailyPlan, useUserProfilePage } from '@app/hooks'; @@ -21,6 +20,8 @@ import ViewsHeaderTabs from './task/daily-plan/views-header-tabs'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; import TaskBlockCard from './task/task-block-card'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; +import { handleDragAndDrop } from '@app/helpers/drag-and-drop'; +import { DragDropContext, Droppable, Draggable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; type FilterOutstanding = 'ALL' | 'DATE'; @@ -136,7 +137,12 @@ export function UserProfilePlans() {
); } - +/** + * + * + * @param {{ profile: any; currentTab?: FilterTabs }} { profile, currentTab = 'All Tasks' } + * @return {*} + */ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; currentTab?: FilterTabs }) { // Filter plans let filteredPlans: IDailyPlan[] = []; @@ -151,122 +157,163 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current const { filteredAllPlanData: filterAllPlanData } = useFilterDateRange(filteredPlans, 'all'); const filterPlans: IDailyPlan[] = currentTab === 'All Tasks' ? filterAllPlanData : filteredPlans; const view = useRecoilValue(dailyPlanViewHeaderTabs); + const [plans, setPlans] = useState(filterPlans); return (
- {filterPlans?.length > 0 ? ( - new Date(plan.date).toISOString().split('T')[0])[0]] - } - > - {filterPlans?.map((plan, index) => ( - - -
-
- {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + {Array.isArray(plans) && plans?.length > 0 ? ( + handleDragAndDrop(result, plans, setPlans)}> + new Date(plan.date).toISOString().split('T')[0])[0]] + } + > + {plans.map((plan, index) => ( + + +
+
+ {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
+
- -
- - - {/* Plan header */} - - - {/* Plan tasks list */} -
    - {plan.tasks?.map((task) => - view === 'CARDS' ? ( - - ) : ( - - ) - )} -
- - {/* Delete Plan */} - {currentTab === 'Today Tasks' && ( - <> - {canSeeActivity ? ( -
- { - setCurrentDeleteIndex(index) - setPopupOpen(prev => !prev); - - }} - 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 confirm*/} + + {/*button cancel*/} + + +
+ ) : ( + <> )} - Delete - - {/*button cancel*/} - - -
- ) : ( - <> + + )} + )} - - )} - -
- ))} -
+ + + + ))} + + ) : ( - + )}
); @@ -280,7 +327,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil // Get all tasks's estimations time const times = - plan.tasks?.map((task) => task.estimate).filter((time): time is number => typeof time === 'number') ?? []; + plan.tasks?.map((task) => task?.estimate).filter((time): time is number => typeof time === 'number') ?? []; let estimatedTime = 0; if (times.length > 0) estimatedTime = times.reduce((acc, cur) => acc + cur, 0) ?? 0; From ba9477c43f13229f1a486b76f3aefa9cded9370b Mon Sep 17 00:00:00 2001 From: Ushindi Gedeon Date: Wed, 17 Jul 2024 19:23:05 +0200 Subject: [PATCH 10/11] [Feature] Add manual time popup page (#2715) * [Feature]create a form to add time * remove undefined object error * Add key to options * Check if null * Replace the Add time button * Place the button between filter and Update the deign of the modal * Delete unused var * Change unfecognised word * Improve the design * Add reusable time picker component * Implement the custom time picker and add a gap of 10min * Update custom date picker * Update style and fix bug when selecting 0h * Fix bug and update styling * Install time picker * Install time picker * Remove TimeKeeper --------- Co-authored-by: cedric karungu --- .../app/[locale]/profile/[memberId]/page.tsx | 6 +- apps/web/lib/features/task/task-filters.tsx | 271 +++++++++++++++++- 2 files changed, 267 insertions(+), 10 deletions(-) diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index 3e3dd3ebf..c44a3d619 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -13,7 +13,7 @@ import { TaskFilter, Timer, TimerStatus, UserProfileTask, getTimerStatusValue, u import { MainHeader, MainLayout } from 'lib/layout'; import Link from 'next/link'; import React, { useCallback, useMemo, useState } from 'react'; -import { useTranslations } from 'next-intl'; +import { useTranslations } from 'next-intl' import stc from 'string-to-color'; import { useRecoilValue, useSetRecoilState } from 'recoil'; @@ -126,7 +126,6 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId {/* User Profile Detail */}
- {profileIsAuthUser && isTrackingEnabled && ( - - {/* Divider */} - {/*
*/} {hook.tab == 'worked' && canSeeActivity && ( diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index e2e1a8659..f098f23fc 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -6,7 +6,9 @@ import { useAuthenticateUser, useDailyPlan, useOrganizationTeams, - useOutsideClick + useOutsideClick, + useModal, + useTeamTasks } from '@app/hooks'; import { IClassName, ITeamTask } from '@app/interfaces'; import { clsxm } from '@app/utils'; @@ -14,16 +16,25 @@ import { Transition } from '@headlessui/react'; import { Button, InputField, Tooltip, VerticalSeparator } from 'lib/components'; import { SearchNormalIcon } from 'assets/svg'; import intersection from 'lodash/intersection'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState, FormEvent } from 'react'; import { TaskUnOrAssignPopover } from './task-assign-popover'; import { TaskLabelsDropdown, TaskPropertiesDropdown, TaskSizesDropdown, TaskStatusDropdown } from './task-status'; import { useTranslations } from 'next-intl'; import { SettingFilterIcon } from 'assets/svg'; import { DailyPlanFilter } from './daily-plan/daily-plan-filter'; +import { Modal, Divider } from 'lib/components'; +import api from '@app/services/client/axios'; +import { MdOutlineMoreTime } from "react-icons/md"; +import { IoIosTimer } from "react-icons/io"; +import { FiLoader } from "react-icons/fi"; +import { DatePicker } from '@components/ui/DatePicker'; +import { PencilSquareIcon } from '@heroicons/react/20/solid'; +import { FaRegCalendarAlt } from "react-icons/fa"; import { useDateRange } from '@app/hooks/useDateRange'; import { TaskDatePickerWithRange } from './task-date-range'; export type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; + type ITabs = { tab: ITab; name: string; @@ -263,6 +274,105 @@ export function TaskFilter({ className, hook, profile }: IClassName & Props) { function InputFilters({ hook, profile }: Props) { const t = useTranslations(); const [loading, setLoading] = useState(false); + const { tasks } = useTeamTasks(); + const { activeTeam } = useOrganizationTeams(); + const members = activeTeam?.members; + + const [date, setDate] = useState(''); + const [isBillable, setIsBillable] = useState(false); + const [startTime, setStartTime] = useState(''); + const [endTime, setEndTime] = useState(''); + const [team, setTeam] = useState(''); + const [task, setTask] = useState(''); + const [description, setDescription] = useState(''); + const [reason, setReason] = useState(''); + const [timeDifference, setTimeDifference] = useState(''); + const [errorMsg, setError] = useState(''); + const [loading1, setLoading1] = useState(false); + + const { isOpen, openModal, closeModal } = useModal(); + + useEffect(() => { + const now = new Date(); + const currentDate = now.toISOString().slice(0, 10); + const currentTime = now.toTimeString().slice(0, 5); + + setDate(currentDate); + setStartTime(currentTime); + setEndTime(currentTime); + }, []); + + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + const timeObject = { + date, + isBillable, + startTime, + endTime, + team, + task, + description, + reason, + timeDifference + }; + + if (date && startTime && endTime && team && task) { + setLoading1(true); + setError(''); + const postData = async () => { + try { + const response = await api.post('/add_time', timeObject); + if (response.data.message) { + setLoading1(false); + closeModal(); + } + + } catch (err) { + setError('Failed to post data'); + setLoading1(false); + } + }; + + postData(); + } else { + setError(`Please complete all required fields with a ${"*"}`) + } + }; + + const calculateTimeDifference = () => { + + const [startHours, startMinutes] = startTime.split(':').map(Number); + const [endHours, endMinutes] = endTime.split(':').map(Number); + + const startTotalMinutes = startHours * 60 + startMinutes; + const endTotalMinutes = endHours * 60 + endMinutes; + + const diffMinutes = endTotalMinutes - startTotalMinutes; + if (diffMinutes < 0) { + return; + } + + const hours = Math.floor(diffMinutes / 60); + const minutes = diffMinutes % 60; + setTimeDifference(`${hours} Hours ${minutes} Minutes`); + }; + + useEffect(() => { + calculateTimeDifference(); + }, [endTime, startTime]); + + useEffect(() => { + if (task == '') { + setTask(tasks[0]?.id); + } + if (team == '') { + members && setTeam(members[0].id); + } + + }, [tasks, members]); + const osSpecificAssignTaskTooltipLabel = 'A'; return ( @@ -294,6 +404,12 @@ function InputFilters({ hook, profile }: Props) { {t('common.FILTER')} + {/* Assign task combobox */} {t('common.ASSIGN_TASK')} -
+
+ + +
+
+ +
+ + { + date ? +
+ + {date} +
+ : ( + + )} +
+ } + selected={new Date()} + onSelect={(dateI) => { + dateI && setDate(dateI.toDateString()); + }} + mode={'single'} + /> +
+
+ +
+ +
setIsBillable(!isBillable)} + style={isBillable ? { background: 'linear-gradient(to right, #3726a662, transparent)' } : { background: '#3726a662' }} + > +
+
+
+
+
+ + setStartTime(e.target.value)} + className="w-full p-2 border text-[13px] font-bold border-gray-300 rounded-[10px]" + required + /> +
+ +
+ + setEndTime(e.target.value)} + className="w-full p-2 border text-[13px] font-bold border-gray-300 rounded-[10px]" + required + /> +
+
+ +
+ +
+ + {timeDifference} +
+
+ +
+ + +
+ +
+ + +
+ +
+ +