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/[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/[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/app/api/daily-plan/[id]/remove/route.ts b/apps/web/app/api/daily-plan/[id]/remove/route.ts new file mode 100644 index 000000000..9d6078491 --- /dev/null +++ b/apps/web/app/api/daily-plan/[id]/remove/route.ts @@ -0,0 +1,23 @@ +import { INextParams, IRemoveTaskFromManyPlans } from "@app/interfaces"; +import { authenticatedGuard } from "@app/services/server/guards/authenticated-guard-app"; +import { deleteDailyPlansManyRequest } from "@app/services/server/requests"; +import { NextResponse } from "next/server"; + +export async function PUT(req: Request, { params }: INextParams) { + const res = new NextResponse(); + const { id } = params; + if (!id) { + return; + } + const { $res, user, access_token } = await authenticatedGuard(req, res); + + if (!user) return $res('Unauthorized'); + + const body = (await req.json()) as unknown as IRemoveTaskFromManyPlans; + const response = await deleteDailyPlansManyRequest({ + data: body, + taskId: id, + bearer_token: access_token, + }); + return $res(response.data); +} diff --git a/apps/web/app/api/daily-plan/[id]/route.ts b/apps/web/app/api/daily-plan/[id]/route.ts index e5cb724e2..121c01bae 100644 --- a/apps/web/app/api/daily-plan/[id]/route.ts +++ b/apps/web/app/api/daily-plan/[id]/route.ts @@ -29,7 +29,6 @@ export async function PUT(req: Request, { params }: INextParams) { if (!id) { return; } - const { $res, user, access_token, tenantId } = await authenticatedGuard(req, res); if (!user) return $res('Unauthorized'); 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/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/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/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/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index d60318923..972aba380 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, @@ -20,14 +20,28 @@ import { getDayPlansByEmployeeAPI, getMyDailyPlansAPI, getPlansByTaskAPI, + removeManyTaskFromPlansAPI, removeTaskFromPlanAPI, updateDailyPlanAPI } from '@app/services/client/api'; -import { ICreateDailyPlan, IDailyPlanTasksUpdate, IUpdateDailyPlan } from '@app/interfaces'; +import { ICreateDailyPlan, IDailyPlanTasksUpdate, IRemoveTaskFromManyPlans, 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); @@ -38,6 +52,8 @@ export function useDailyPlan() { const { loading: addTaskToPlanLoading, queryCall: addTaskToPlanQueryCall } = useQuery(addTaskToPlanAPI); const { loading: removeTaskFromPlanLoading, queryCall: removeTAskFromPlanQueryCall } = useQuery(removeTaskFromPlanAPI); + const { loading: removeManyTaskFromPlanLoading, queryCall: removeManyTaskPlanQueryCall } = useQuery(removeManyTaskFromPlansAPI); + const { loading: deleteDailyPlanLoading, queryCall: deleteDailyPlanQueryCall } = useQuery(deleteDailyPlanAPI); const [dailyPlan, setDailyPlan] = useRecoilState(dailyPlanListState); @@ -148,6 +164,7 @@ export function useDailyPlan() { ] ); + const removeTaskFromPlan = useCallback( async (data: IDailyPlanTasksUpdate, planId: string) => { const res = await removeTAskFromPlanQueryCall(data, planId); @@ -168,6 +185,42 @@ export function useDailyPlan() { ] ); + const removeManyTaskPlans = useCallback( + async (data: IRemoveTaskFromManyPlans, taskId: string) => { + const res = await removeManyTaskPlanQueryCall({ taskId, data }); + const updatedProfileDailyPlans = profileDailyPlans.items + .map((plan) => { + const updatedTasks = plan.tasks ? plan.tasks.filter((task) => task.id !== taskId) : []; + return { ...plan, tasks: updatedTasks }; + }) + .filter((plan) => plan.tasks && plan.tasks.length > 0); + // Delete plans without tasks + const updatedEmployeePlans = employeePlans + .map((plan) => { + const updatedTasks = plan.tasks ? plan.tasks.filter((task) => task.id !== taskId) : []; + return { ...plan, tasks: updatedTasks }; + }) + .filter((plan) => plan.tasks && plan.tasks.length > 0); + + setProfileDailyPlans({ + total: profileDailyPlans.total, + items: updatedProfileDailyPlans + }); + setEmployeePlans(updatedEmployeePlans); + getMyDailyPlans(); + return res; + + }, + [ + removeManyTaskPlanQueryCall, + employeePlans, + getMyDailyPlans, + profileDailyPlans, + setEmployeePlans, + setProfileDailyPlans + ] + ) + const deleteDailyPlan = useCallback( async (planId: string) => { const res = await deleteDailyPlanQueryCall(planId); @@ -241,10 +294,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, @@ -285,6 +362,9 @@ export function useDailyPlan() { removeTaskFromPlan, removeTaskFromPlanLoading, + removeManyTaskPlans, + removeManyTaskFromPlanLoading, + deleteDailyPlan, deleteDailyPlanLoading, @@ -292,6 +372,9 @@ export function useDailyPlan() { pastPlans, outstandingPlans, todayPlan, - sortedPlans + sortedPlans, + + addTodayPlanTrigger, + setAddTodayPlanTrigger }; } diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts index 432a452c1..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; @@ -205,11 +210,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 && @@ -423,6 +425,7 @@ export function useTimer() { startTimer, stopTimer, hasPlan, + hasPlanForTomorrow, canRunTimer, canTrack, isPlanVerified, @@ -463,6 +466,7 @@ export function useTimerView() { startTimer, stopTimer, hasPlan, + hasPlanForTomorrow, canRunTimer, canTrack, isPlanVerified, @@ -494,6 +498,7 @@ export function useTimerView() { timerStatus, activeTeamTask, hasPlan, + hasPlanForTomorrow, disabled: !canRunTimer, canTrack, isPlanVerified, 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/interfaces/IDailyPlan.ts b/apps/web/app/interfaces/IDailyPlan.ts index e5003ef02..5d91cd89a 100644 --- a/apps/web/app/interfaces/IDailyPlan.ts +++ b/apps/web/app/interfaces/IDailyPlan.ts @@ -1,5 +1,6 @@ import { IBasePerTenantAndOrganizationEntity } from './IBaseModel'; -import { IRelationnalEmployee } from './IEmployee'; +import { IEmployee, IRelationnalEmployee } from './IEmployee'; +import { IOrganization } from './IOrganization'; import { ITeamTask } from './ITask'; export interface IDailyPlanBase extends IBasePerTenantAndOrganizationEntity { @@ -8,6 +9,12 @@ export interface IDailyPlanBase extends IBasePerTenantAndOrganizationEntity { status: DailyPlanStatusEnum; } +export interface IRemoveTaskFromManyPlans { + employeeId?: IEmployee['id']; + plansIds?: IDailyPlan['id'][]; + organizationId?: IOrganization['id']; +} + export interface IDailyPlan extends IDailyPlanBase, IRelationnalEmployee { tasks?: ITeamTask[]; } 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; diff --git a/apps/web/app/services/client/api/daily-plan.ts b/apps/web/app/services/client/api/daily-plan.ts index a99f1596e..7ae9886d7 100644 --- a/apps/web/app/services/client/api/daily-plan.ts +++ b/apps/web/app/services/client/api/daily-plan.ts @@ -5,6 +5,7 @@ import { ICreateDailyPlan, IDailyPlan, IDailyPlanTasksUpdate, + IRemoveTaskFromManyPlans, IUpdateDailyPlan, PaginationResponse } from '@app/interfaces'; @@ -14,7 +15,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 +34,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 +53,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, @@ -105,6 +106,11 @@ export function removeTaskFromPlanAPI(data: IDailyPlanTasksUpdate, planId: strin return put(`/daily-plan/${planId}/task`, { ...data, organizationId }, { tenantId }); } +export function removeManyTaskFromPlansAPI({ taskId, data }: { taskId: string, data: IRemoveTaskFromManyPlans }) { + const organizationId = getOrganizationIdCookie(); + return put(`/daily-plan/${taskId}/remove`, { ...data, organizationId }) +} + export function deleteDailyPlanAPI(planId: string) { return deleteApi(`/daily-plan/${planId}`); } diff --git a/apps/web/app/services/server/requests/daily-plan.ts b/apps/web/app/services/server/requests/daily-plan.ts index 11f451d4a..cfe4655c7 100644 --- a/apps/web/app/services/server/requests/daily-plan.ts +++ b/apps/web/app/services/server/requests/daily-plan.ts @@ -1,5 +1,5 @@ import qs from 'qs'; -import { ICreateDailyPlan, IDailyPlan, IDailyPlanTasksUpdate, IUpdateDailyPlan } from '@app/interfaces/IDailyPlan'; +import { ICreateDailyPlan, IDailyPlan, IDailyPlanTasksUpdate, IRemoveTaskFromManyPlans, IUpdateDailyPlan } from '@app/interfaces/IDailyPlan'; import { serverFetch } from '../fetch'; import { DeleteResponse } from '@app/interfaces'; @@ -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; @@ -202,3 +202,12 @@ export function deleteDailyPlanRequest({ planId, bearer_token }: { planId: strin bearer_token }); } + +export function deleteDailyPlansManyRequest({ bearer_token, taskId, data }: { bearer_token?: string, taskId: string, data: IRemoveTaskFromManyPlans }) { + return serverFetch({ + method: 'PUT', + path: `/daily-plan/${taskId}/remove`, + body: data, + bearer_token, + }); +} diff --git a/apps/web/app/stores/daily-plan.ts b/apps/web/app/stores/daily-plan.ts index ab4b53e8b..e28f6e1df 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,63 @@ 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/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/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/components/modal.tsx b/apps/web/lib/components/modal.tsx index 0742cb2e8..592e7bed1 100644 --- a/apps/web/lib/components/modal.tsx +++ b/apps/web/lib/components/modal.tsx @@ -52,9 +52,8 @@ export function Modal({ {description && {description}}
) : (
--
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..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 @@ -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,10 +32,12 @@ 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(); + 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); @@ -50,7 +52,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 +106,14 @@ export function CreateDailyPlanFormModal({ /> )} - + /> */} {planMode === 'custom' && ( @@ -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/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/daily-plan/future-tasks.tsx b/apps/web/lib/features/task/daily-plan/future-tasks.tsx index ff4af85e1..d235ca163 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'; @@ -12,115 +12,160 @@ 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'; +import { IDailyPlan } from '@app/interfaces'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; 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 [currentDeleteIndex, setCurrentDeleteIndex] = useState(0); + const [futureDailyPlanTasks, setFutureDailyPlanTasks] = useState(filteredFuturePlanData); const view = useRecoilValue(dailyPlanViewHeaderTabs); return (
- {futurePlans.length > 0 ? ( - 0 ? ( + handleDragAndDrop(result, futureDailyPlanTasks, setFutureDailyPlanTasks)} > - {futurePlans.map((plan) => ( - - -
-
- {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(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" - > - 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 c0ca5b92d..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; @@ -14,48 +18,91 @@ 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 || []), []); + const [task, setTask] = useState(tasks!); + 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' ? ( - - ) : ( - - ); - })} -
- - ))} + handleDragAndDropDailyOutstandingAll(result, task, setTask)} + > + {/* */} + + {(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 249f3ebdd..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,6 +1,6 @@ -import { formatDayPlanDate, yesterdayDate } from '@app/helpers'; +import { formatDayPlanDate, handleDragAndDrop, 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'; @@ -8,68 +8,120 @@ import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; 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 }: { profile: any }) { +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 (
- {pastPlans?.length > 0 ? ( - - {pastPlans?.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-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 3b6c7544b..7ba6f64f3 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 }; }); @@ -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 192c035fd..64b03790d 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 @@ -132,9 +133,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 }; }) || []; @@ -160,6 +161,8 @@ export function TaskCard(props: Props) { className="px-4 w-full" taskBadgeClassName={clsxm(taskBadgeClassName)} taskTitleClassName={clsxm(taskTitleClassName)} + dayPlanTab={planMode} + tab={viewType} />
@@ -248,7 +251,7 @@ export function TaskCard(props: Props) { )} */}
- {' '} + {' '} {viewType === 'default' && ( <>
@@ -416,6 +419,7 @@ function TimerButtonCall({ open={isOpen} plan={hasPlan} startTimer={startTimer} + hasPlan={!!hasPlan} /> ); @@ -427,8 +431,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 +465,7 @@ function TaskInfo({ )} {/* Task status */} - {task && } + {task && } {!task &&
--
}
); @@ -492,6 +500,7 @@ function TaskCardMenu({ }, [memberInfo, task, viewType]); const canSeeActivity = useCanSeeActivityScreen(); + const { hasPlan, hasPlanForTomorrow } = useTimerView(); return ( @@ -548,6 +557,7 @@ function TaskCardMenu({ planMode="today" taskId={task.id} employeeId={profile?.member?.employeeId ?? ''} + hasTodayPlan={hasPlan} />
  • @@ -555,6 +565,7 @@ function TaskCardMenu({ planMode="tomorow" taskId={task.id} employeeId={profile?.member?.employeeId ?? ''} + hasPlanForTomorrow={hasPlanForTomorrow} />
  • @@ -591,6 +602,12 @@ function TaskCardMenu({ plan={plan} />
  • +
    + +
    ) : ( <> @@ -628,12 +645,16 @@ export function PlanTask({ planMode, taskId, employeeId, - chooseMember + chooseMember, + hasTodayPlan, + hasPlanForTomorrow }: { taskId: string; planMode: IDailyPlanMode; employeeId?: string; chooseMember?: boolean; + hasTodayPlan?: IDailyPlan; + hasPlanForTomorrow?: IDailyPlan; }) { const t = useTranslations(); const [isPending, startTransition] = useTransition(); @@ -688,7 +709,7 @@ export function PlanTask({ employeeId={employeeId} chooseMember={chooseMember} /> - {planMode === 'today' && ( + {planMode === 'today' && !hasTodayPlan && ( {isPending ? ( @@ -697,7 +718,7 @@ export function PlanTask({ )} )} - {planMode === 'tomorow' && ( + {planMode === 'tomorow' && !hasPlanForTomorrow && ( {isPending ? ( @@ -748,3 +769,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/task/task-date-range.tsx b/apps/web/lib/features/task/task-date-range.tsx new file mode 100644 index 000000000..1f5c5d243 --- /dev/null +++ b/apps/web/lib/features/task/task-date-range.tsx @@ -0,0 +1,71 @@ +"use client" + +import React from "react" +import { format } from "date-fns" +import { Calendar as CalendarIcon } from "lucide-react" +import { cn } from "lib/utils" +import { Button } from "@components/ui/button" +import { Calendar } from "@components/ui/calendar" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@components/ui/popover" +import { DateRange } from "react-day-picker" +import { SetterOrUpdater } from "recoil" + +interface ITaskDatePickerWithRange { + className?: React.HTMLAttributes, + date?: DateRange; + onSelect?: SetterOrUpdater; + label?: string +} + +export function TaskDatePickerWithRange({ + className, + date, + onSelect, + label + +}: ITaskDatePickerWithRange) { + return ( +
    + + + + + + + + +
    + ) +} diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index 72b3d2c22..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,14 +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 ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; type ITabs = { tab: ITab; name: string; @@ -261,7 +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 ( @@ -293,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} +
    +
    + +
    + + +
    + +
    + + +
    + +
    + +