diff --git a/.cspell.json b/.cspell.json index 7d564cd35..3157fbb5e 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,6 +4,7 @@ "caseSensitive": false, "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", "words": [ + "Unplan", "nivo", "accepte", "Accordian", diff --git a/.github/workflows/desktop-server-api.apps.yml b/.github/workflows/desktop-server-api.apps.yml index 58bfab1f8..afe85a1d9 100644 --- a/.github/workflows/desktop-server-api.apps.yml +++ b/.github/workflows/desktop-server-api.apps.yml @@ -27,7 +27,7 @@ jobs: ref: master - name: Install Node.js, NPM and Yarn - uses: buildjet/setup-node@v3 + uses: buildjet/setup-node@v4 with: node-version: '20.11.1' cache: 'yarn' @@ -267,6 +267,10 @@ jobs: ChocolateyInstall: '' ChromeWebDriver: '' COBERTURA_HOME: '' + # COMPUTERNAME: '' + # COMSPEC: '' + # CONDA: '' + # DEPLOYMENT_BASEPATH: '' SBT_HOME: '' SELENIUM_JAR_PATH: '' STATS_BLT: '' @@ -287,10 +291,49 @@ jobs: ANDROID_NDK_LATEST_HOME: '' ANDROID_NDK_ROOT: '' ANDROID_SDK_ROOT: '' + # GITHUB_ACTION: '' + # GITHUB_ACTIONS: '' + # GITHUB_ACTION_REF: '' + # GITHUB_ACTION_REPOSITORY: '' + # GITHUB_ACTOR: '' + # GITHUB_ACTOR_ID: '' + # GITHUB_API_URL: '' + # GITHUB_BASE_REF: '' + # GITHUB_ENV: '' + # GITHUB_EVENT_NAME: '' + # GITHUB_EVENT_PATH: '' + # GITHUB_GRAPHQL_URL: '' + # GITHUB_HEAD_REF: '' + # GITHUB_JOB: '' + # GITHUB_OUTPUT: '' + # GITHUB_PATH: '' + # GITHUB_REF: '' + # GITHUB_REF_NAME: '' + # GITHUB_REF_PROTECTED: '' + # GITHUB_REF_TYPE: '' + # GITHUB_REPOSITORY: '' + # GITHUB_REPOSITORY_ID: '' + # GITHUB_REPOSITORY_OWNER: '' + # GITHUB_REPOSITORY_OWNER_ID: '' + # GITHUB_RETENTION_DAYS: '' + # GITHUB_RUN_ATTEMPT: '' + # GITHUB_RUN_ID: '' + # GITHUB_RUN_NUMBER: '' + # GITHUB_SERVER_URL: '' + # GITHUB_SHA: '' + # GITHUB_STATE: '' + # GITHUB_STEP_SUMMARY: '' + # GITHUB_TRIGGERING_ACTOR: '' + # GITHUB_WORKFLOW: '' + # GITHUB_WORKFLOW_REF: '' + # GITHUB_WORKFLOW_SHA: '' + # GITHUB_WORKSPACE: '' GOROOT_1_20_X64: '' GOROOT_1_21_X64: '' GOROOT_1_22_X64: '' GRADLE_HOME: '' + # HOMEDRIVE: '' + # HOMEPATH: '' IEWebDriver: '' ImageOS: '' ImageVersion: '' @@ -299,10 +342,17 @@ jobs: JAVA_HOME_17_X64: '' JAVA_HOME_21_X64: '' JAVA_HOME_8_X64: '' + # LOCALAPPDATA: '' + # LOGONSERVER: '' M2: '' M2_REPO: '' MAVEN_OPTS: '' MonAgentClientLocation: '' + # npm_config_prefix: '' + # NUMBER_OF_PROCESSORS: '' + # OS: '' + # PATHEXT: '' + # PERFLOG_LOCATION_SETTING: '' PGBIN: '' PGDATA: '' PGPASSWORD: '' @@ -319,6 +369,7 @@ jobs: PROCESSOR_REVISION: '' PSModuleAnalysisCachePath: '' PSModulePath: '' + Path: 'C:\hostedtoolcache\windows\node\20.11.1\x64;C:\Program Files\Git\bin;C:\npm\prefix;C:\hostedtoolcache\windows\Python\3.9.13\x64\Scripts;C:\hostedtoolcache\windows\Python\3.9.13\x64;C:\Program Files\OpenSSL\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\PowerShell\7\;C:\Program Files\CMake\bin;C:\Program Files\nodejs\;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\GitHub CLI\;c:\tools\php;C:\Program Files (x86)\sbt\bin;C:\Program Files\Amazon\AWSCLIV2\;C:\Users\runneradmin\.dotnet\tools;C:\Users\runneradmin\.cargo\bin;C:\Users\runneradmin\AppData\Local\Microsoft\WindowsApps' DOTNET_MULTILEVEL_LOOKUP: '' DOTNET_NOLOGO: '' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '' @@ -328,6 +379,7 @@ jobs: GeckoWebDriver: '' GHCUP_INSTALL_BASE_PREFIX: '' GHCUP_MSYS2: '' + # RTOOLS44_HOME: '' RUNNER_ARCH: '' RUNNER_ENVIRONMENT: '' RUNNER_NAME: '' @@ -337,8 +389,20 @@ jobs: RUNNER_TOOL_CACHE: '' RUNNER_TRACKING_ID: '' RUNNER_WORKSPACE: '' + # USERDOMAIN: '' + # USERDOMAIN_ROAMINGPROFILE: '' + # USERNAME: '' + # USERPROFILE: '' + # VCPKG_INSTALLATION_ROOT: '' + # WIX: '' + # TERM: '' + # HOME: '' + # WINDIR: '' + # ProgramData: '' # PROGRAMFILES: '' # ProgramW6432: '' # ALLUSERSPROFILE: '' # APPDATA: '' # COMMONPROGRAMFILES: '' + # CommonProgramFiles(x86) + # CommonProgramW6432 diff --git a/.github/workflows/desktop-server-web.apps.yml b/.github/workflows/desktop-server-web.apps.yml index 2f921a7bc..2fcc04f92 100644 --- a/.github/workflows/desktop-server-web.apps.yml +++ b/.github/workflows/desktop-server-web.apps.yml @@ -258,6 +258,10 @@ jobs: ChocolateyInstall: '' ChromeWebDriver: '' COBERTURA_HOME: '' + # COMPUTERNAME: '' + # COMSPEC: '' + # CONDA: '' + # DEPLOYMENT_BASEPATH: '' SBT_HOME: '' SELENIUM_JAR_PATH: '' STATS_BLT: '' @@ -278,10 +282,49 @@ jobs: ANDROID_NDK_LATEST_HOME: '' ANDROID_NDK_ROOT: '' ANDROID_SDK_ROOT: '' + # GITHUB_ACTION: '' + # GITHUB_ACTIONS: '' + # GITHUB_ACTION_REF: '' + # GITHUB_ACTION_REPOSITORY: '' + # GITHUB_ACTOR: '' + # GITHUB_ACTOR_ID: '' + # GITHUB_API_URL: '' + # GITHUB_BASE_REF: '' + # GITHUB_ENV: '' + # GITHUB_EVENT_NAME: '' + # GITHUB_EVENT_PATH: '' + # GITHUB_GRAPHQL_URL: '' + # GITHUB_HEAD_REF: '' + # GITHUB_JOB: '' + # GITHUB_OUTPUT: '' + # GITHUB_PATH: '' + # GITHUB_REF: '' + # GITHUB_REF_NAME: '' + # GITHUB_REF_PROTECTED: '' + # GITHUB_REF_TYPE: '' + # GITHUB_REPOSITORY: '' + # GITHUB_REPOSITORY_ID: '' + # GITHUB_REPOSITORY_OWNER: '' + # GITHUB_REPOSITORY_OWNER_ID: '' + # GITHUB_RETENTION_DAYS: '' + # GITHUB_RUN_ATTEMPT: '' + # GITHUB_RUN_ID: '' + # GITHUB_RUN_NUMBER: '' + # GITHUB_SERVER_URL: '' + # GITHUB_SHA: '' + # GITHUB_STATE: '' + # GITHUB_STEP_SUMMARY: '' + # GITHUB_TRIGGERING_ACTOR: '' + # GITHUB_WORKFLOW: '' + # GITHUB_WORKFLOW_REF: '' + # GITHUB_WORKFLOW_SHA: '' + # GITHUB_WORKSPACE: '' GOROOT_1_20_X64: '' GOROOT_1_21_X64: '' GOROOT_1_22_X64: '' GRADLE_HOME: '' + # HOMEDRIVE: '' + # HOMEPATH: '' IEWebDriver: '' ImageOS: '' ImageVersion: '' @@ -290,10 +333,17 @@ jobs: JAVA_HOME_17_X64: '' JAVA_HOME_21_X64: '' JAVA_HOME_8_X64: '' + # LOCALAPPDATA: '' + # LOGONSERVER: '' M2: '' M2_REPO: '' MAVEN_OPTS: '' MonAgentClientLocation: '' + # npm_config_prefix: '' + # NUMBER_OF_PROCESSORS: '' + # OS: '' + # PATHEXT: '' + # PERFLOG_LOCATION_SETTING: '' PGBIN: '' PGDATA: '' PGPASSWORD: '' @@ -310,6 +360,7 @@ jobs: PROCESSOR_REVISION: '' PSModuleAnalysisCachePath: '' PSModulePath: '' + Path: 'C:\hostedtoolcache\windows\node\20.11.1\x64;C:\Program Files\Git\bin;C:\npm\prefix;C:\hostedtoolcache\windows\Python\3.9.13\x64\Scripts;C:\hostedtoolcache\windows\Python\3.9.13\x64;C:\Program Files\OpenSSL\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\PowerShell\7\;C:\Program Files\CMake\bin;C:\Program Files\nodejs\;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\GitHub CLI\;c:\tools\php;C:\Program Files (x86)\sbt\bin;C:\Program Files\Amazon\AWSCLIV2\;C:\Users\runneradmin\.dotnet\tools;C:\Users\runneradmin\.cargo\bin;C:\Users\runneradmin\AppData\Local\Microsoft\WindowsApps' DOTNET_MULTILEVEL_LOOKUP: '' DOTNET_NOLOGO: '' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '' @@ -319,6 +370,7 @@ jobs: GeckoWebDriver: '' GHCUP_INSTALL_BASE_PREFIX: '' GHCUP_MSYS2: '' + # RTOOLS44_HOME: '' RUNNER_ARCH: '' RUNNER_ENVIRONMENT: '' RUNNER_NAME: '' @@ -328,8 +380,20 @@ jobs: RUNNER_TOOL_CACHE: '' RUNNER_TRACKING_ID: '' RUNNER_WORKSPACE: '' + # USERDOMAIN: '' + # USERDOMAIN_ROAMINGPROFILE: '' + # USERNAME: '' + # USERPROFILE: '' + # VCPKG_INSTALLATION_ROOT: '' + # WIX: '' + # TERM: '' + # HOME: '' + # WINDIR: '' + # ProgramData: '' # PROGRAMFILES: '' # ProgramW6432: '' # ALLUSERSPROFILE: '' # APPDATA: '' # COMMONPROGRAMFILES: '' + # CommonProgramFiles(x86) + # CommonProgramW6432 diff --git a/.github/workflows/desktop.apps.yml b/.github/workflows/desktop.apps.yml index 8a192a06e..618ef3814 100644 --- a/.github/workflows/desktop.apps.yml +++ b/.github/workflows/desktop.apps.yml @@ -27,7 +27,7 @@ jobs: ref: master - name: Install Node.js, NPM and Yarn - uses: buildjet/setup-node@v3 + uses: buildjet/setup-node@v4 with: node-version: '20.11.1' cache: 'yarn' @@ -267,6 +267,10 @@ jobs: ChocolateyInstall: '' ChromeWebDriver: '' COBERTURA_HOME: '' + # COMPUTERNAME: '' + # COMSPEC: '' + # CONDA: '' + # DEPLOYMENT_BASEPATH: '' SBT_HOME: '' SELENIUM_JAR_PATH: '' STATS_BLT: '' @@ -287,10 +291,49 @@ jobs: ANDROID_NDK_LATEST_HOME: '' ANDROID_NDK_ROOT: '' ANDROID_SDK_ROOT: '' + # GITHUB_ACTION: '' + # GITHUB_ACTIONS: '' + # GITHUB_ACTION_REF: '' + # GITHUB_ACTION_REPOSITORY: '' + # GITHUB_ACTOR: '' + # GITHUB_ACTOR_ID: '' + # GITHUB_API_URL: '' + # GITHUB_BASE_REF: '' + # GITHUB_ENV: '' + # GITHUB_EVENT_NAME: '' + # GITHUB_EVENT_PATH: '' + # GITHUB_GRAPHQL_URL: '' + # GITHUB_HEAD_REF: '' + # GITHUB_JOB: '' + # GITHUB_OUTPUT: '' + # GITHUB_PATH: '' + # GITHUB_REF: '' + # GITHUB_REF_NAME: '' + # GITHUB_REF_PROTECTED: '' + # GITHUB_REF_TYPE: '' + # GITHUB_REPOSITORY: '' + # GITHUB_REPOSITORY_ID: '' + # GITHUB_REPOSITORY_OWNER: '' + # GITHUB_REPOSITORY_OWNER_ID: '' + # GITHUB_RETENTION_DAYS: '' + # GITHUB_RUN_ATTEMPT: '' + # GITHUB_RUN_ID: '' + # GITHUB_RUN_NUMBER: '' + # GITHUB_SERVER_URL: '' + # GITHUB_SHA: '' + # GITHUB_STATE: '' + # GITHUB_STEP_SUMMARY: '' + # GITHUB_TRIGGERING_ACTOR: '' + # GITHUB_WORKFLOW: '' + # GITHUB_WORKFLOW_REF: '' + # GITHUB_WORKFLOW_SHA: '' + # GITHUB_WORKSPACE: '' GOROOT_1_20_X64: '' GOROOT_1_21_X64: '' GOROOT_1_22_X64: '' GRADLE_HOME: '' + # HOMEDRIVE: '' + # HOMEPATH: '' IEWebDriver: '' ImageOS: '' ImageVersion: '' @@ -299,10 +342,17 @@ jobs: JAVA_HOME_17_X64: '' JAVA_HOME_21_X64: '' JAVA_HOME_8_X64: '' + # LOCALAPPDATA: '' + # LOGONSERVER: '' M2: '' M2_REPO: '' MAVEN_OPTS: '' MonAgentClientLocation: '' + # npm_config_prefix: '' + # NUMBER_OF_PROCESSORS: '' + # OS: '' + # PATHEXT: '' + # PERFLOG_LOCATION_SETTING: '' PGBIN: '' PGDATA: '' PGPASSWORD: '' @@ -319,6 +369,7 @@ jobs: PROCESSOR_REVISION: '' PSModuleAnalysisCachePath: '' PSModulePath: '' + Path: 'C:\hostedtoolcache\windows\node\20.11.1\x64;C:\Program Files\Git\bin;C:\npm\prefix;C:\hostedtoolcache\windows\Python\3.9.13\x64\Scripts;C:\hostedtoolcache\windows\Python\3.9.13\x64;C:\Program Files\OpenSSL\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\PowerShell\7\;C:\Program Files\CMake\bin;C:\Program Files\nodejs\;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\GitHub CLI\;c:\tools\php;C:\Program Files (x86)\sbt\bin;C:\Program Files\Amazon\AWSCLIV2\;C:\Users\runneradmin\.dotnet\tools;C:\Users\runneradmin\.cargo\bin;C:\Users\runneradmin\AppData\Local\Microsoft\WindowsApps' DOTNET_MULTILEVEL_LOOKUP: '' DOTNET_NOLOGO: '' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '' @@ -328,6 +379,7 @@ jobs: GeckoWebDriver: '' GHCUP_INSTALL_BASE_PREFIX: '' GHCUP_MSYS2: '' + # RTOOLS44_HOME: '' RUNNER_ARCH: '' RUNNER_ENVIRONMENT: '' RUNNER_NAME: '' @@ -337,8 +389,20 @@ jobs: RUNNER_TOOL_CACHE: '' RUNNER_TRACKING_ID: '' RUNNER_WORKSPACE: '' + # USERDOMAIN: '' + # USERDOMAIN_ROAMINGPROFILE: '' + # USERNAME: '' + # USERPROFILE: '' + # VCPKG_INSTALLATION_ROOT: '' + # WIX: '' + # TERM: '' + # HOME: '' + # WINDIR: '' + # ProgramData: '' # PROGRAMFILES: '' # ProgramW6432: '' # ALLUSERSPROFILE: '' # APPDATA: '' # COMMONPROGRAMFILES: '' + # CommonProgramFiles(x86) + # CommonProgramW6432 diff --git a/apps/extensions/yarn.lock b/apps/extensions/yarn.lock index ac4ff1cdd..fd1d0ef18 100644 --- a/apps/extensions/yarn.lock +++ b/apps/extensions/yarn.lock @@ -2355,7 +2355,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -3930,11 +3930,11 @@ merge2@^1.3.0, merge2@^1.4.1: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime@^1.4.1: diff --git a/apps/mobile/yarn.lock b/apps/mobile/yarn.lock index 3490bbf7a..fad785b47 100644 --- a/apps/mobile/yarn.lock +++ b/apps/mobile/yarn.lock @@ -4415,7 +4415,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -10373,11 +10373,11 @@ metro@0.76.0: yargs "^17.5.1" micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": diff --git a/apps/web/app/[locale]/calendar/component.tsx b/apps/web/app/[locale]/calendar/component.tsx index 4be0c2305..6656ec634 100644 --- a/apps/web/app/[locale]/calendar/component.tsx +++ b/apps/web/app/[locale]/calendar/component.tsx @@ -18,6 +18,7 @@ import React from "react"; import { DateRange } from "react-day-picker"; import { LuCalendarDays } from "react-icons/lu"; import { Input } from "@components/ui/input"; +import { SettingFilterIcon } from "assets/svg"; export function HeadCalendar({ openModal, @@ -60,14 +61,19 @@ export function HeadCalendar({ } -export function HeadTimeSheet({ timesheet }: { timesheet?: timesheetCalendar }) { +export function HeadTimeSheet({ timesheet, isOpen, openModal, closeModal }: { timesheet?: timesheetCalendar, isOpen?: boolean, openModal?: () => void, closeModal?: () => void }) { const [date, setDate] = React.useState({ from: new Date(2022, 0, 20), to: addDays(new Date(2022, 0, 20), 20) - }); + }) return ( +
+
{timesheet === 'TimeSheet' && (
@@ -123,7 +129,17 @@ export function HeadTimeSheet({ timesheet }: { timesheet?: timesheetCalendar }) />
- +
diff --git a/apps/web/app/[locale]/calendar/page.tsx b/apps/web/app/[locale]/calendar/page.tsx index dadeaf474..5bf66af03 100644 --- a/apps/web/app/[locale]/calendar/page.tsx +++ b/apps/web/app/[locale]/calendar/page.tsx @@ -61,6 +61,7 @@ const CalendarPage = () => { closeModal={closeManualTimeModal} isOpen={isManualTimeModalOpen} params='AddManuelTime' + timeSheetStatus='ManagerTimesheet' />
+
+
+ + +
+ + {/* */} + + + + {/* */} + + {/* */} + {task && } +
+
+
+
+ +
+ +
+
+
+ ); +} diff --git a/apps/web/app/[locale]/task/[id]/page.tsx b/apps/web/app/[locale]/task/[id]/page.tsx index 4c029a475..4b9422499 100644 --- a/apps/web/app/[locale]/task/[id]/page.tsx +++ b/apps/web/app/[locale]/task/[id]/page.tsx @@ -1,12 +1,6 @@ 'use client'; import { useOrganizationTeams, useTeamTasks, useUserProfilePage } from '@app/hooks'; -import { ChildIssueCard } from '@components/pages/task/ChildIssueCard'; -import { RelatedIssueCard } from '@components/pages/task/IssueCard'; -import TaskProperties from '@components/pages/task/TaskProperties'; -import RichTextEditor from '@components/pages/task/description-block/task-description-editor'; -import TaskDetailsAside from '@components/pages/task/task-details-aside'; -import TaskTitleBlock from '@components/pages/task/title-block/task-title-block'; import { withAuthentication } from 'lib/app/authenticator'; import { Breadcrumb, Container } from 'lib/components'; import { ArrowLeftIcon } from 'assets/svg'; @@ -17,7 +11,7 @@ import { useTranslations } from 'next-intl'; import { fullWidthState } from '@app/stores/fullWidth'; import { useRecoilValue } from 'recoil'; -import { TaskActivity } from 'lib/features/task/task-activity'; +import { TaskDetailsComponent } from './component'; const TaskDetails = () => { const profile = useUserProfilePage(); @@ -71,76 +65,11 @@ const TaskDetails = () => { -
-
-
- - -
- - {/* */} - - - - {/* */} - - {/* */} - {task && } -
-
-
-
- -
- -
-
-
- + {task && } {/* */}
); }; -/** -function IssueModal({ task }: { task: ITeamTask | null }) { - const { handleStatusUpdate } = useTeamTasks(); - const { trans } = useTranslation(); - const modal = useModal(); - - const { openModal } = modal; - - const handleChange = useCallback( - (status: any) => { - handleStatusUpdate(status, 'issueType', task); - }, - [task, handleStatusUpdate] - ); - - useEffect(() => { - if ( - task?.createdAt && - task?.updatedAt && - task?.createdAt === task?.updatedAt - ) { - openModal(); - } - }, [task?.updatedAt, task?.createdAt, openModal]); - - return ( - - <> - - ); -} - */ - export default withAuthentication(TaskDetails, { displayName: 'TaskDetails' }); diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index 5ecc3f76d..9efedc8d0 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -273,6 +273,7 @@ export const USER_SAW_OUTSTANDING_NOTIFICATION = 'user-saw-notif'; export const DAILY_PLAN_SUGGESTION_MODAL_DATE = 'daily-plan-suggestion-modal-date'; export const TASKS_ESTIMATE_HOURS_MODAL_DATE = 'tasks-estimate-hours-modal-date'; export const DAILY_PLAN_ESTIMATE_HOURS_MODAL_DATE = 'daily-plan-estimate-hours-modal'; +export const DEFAULT_PLANNED_TASK_ID = 'default-planned-task-id'; // OAuth providers keys @@ -315,7 +316,7 @@ export const manualTimeReasons: ManualTimeReasons[] = [ ]; export const statusOptions = [ - { value: "Approved", label: "Approved" }, - { value: "Pending", label: "Pending" }, - { value: "Rejected", label: "Rejected" }, + { value: 'Approved', label: 'Approved' }, + { value: 'Pending', label: 'Pending' }, + { value: 'Rejected', label: 'Rejected' } ]; diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index 6e849626f..c8a561547 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -1,9 +1,10 @@ 'use client'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { useCallback, useEffect } from 'react'; import { useQuery } from '../useQuery'; import { + activeTeamState, dailyPlanFetchingState, dailyPlanListState, employeePlansListState, @@ -31,6 +32,7 @@ export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Ta export function useDailyPlan() { const { user } = useAuthenticateUser(); + const activeTeam = useRecoilValue(activeTeamState); const { loading, queryCall } = useQuery(getDayPlansByEmployeeAPI); const { loading: getAllDayPlansLoading, queryCall: getAllQueryCall } = useQuery(getAllDayPlansAPI); @@ -101,7 +103,10 @@ export function useDailyPlan() { const createDailyPlan = useCallback( async (data: ICreateDailyPlan) => { if (user?.tenantId) { - const res = await createQueryCall(data, user?.tenantId || ''); + const res = await createQueryCall( + { ...data, organizationTeamId: activeTeam?.id }, + user?.tenantId || '' + ); //Check if there is an existing plan const isPlanExist = profileDailyPlans.items.find((plan) => plan.date?.toString()?.startsWith(new Date(data.date)?.toISOString().split('T')[0]) @@ -132,6 +137,7 @@ export function useDailyPlan() { } }, [ + activeTeam?.id, createQueryCall, employeePlans, getMyDailyPlans, @@ -149,9 +155,21 @@ export function useDailyPlan() { const updatedEmployee = employeePlans.filter((plan) => plan.id != planId); setProfileDailyPlans({ total: profileDailyPlans.total, items: [...updated, res.data] }); setEmployeePlans([...updatedEmployee, res.data]); + // Fetch updated plans + getMyDailyPlans(); + getAllDayPlans(); return res; }, - [employeePlans, profileDailyPlans, setEmployeePlans, setProfileDailyPlans, updateQueryCall] + [ + employeePlans, + getAllDayPlans, + getMyDailyPlans, + profileDailyPlans.items, + profileDailyPlans.total, + setEmployeePlans, + setProfileDailyPlans, + updateQueryCall + ] ); const addTaskToPlan = useCallback( diff --git a/apps/web/app/hooks/features/useEmployee.ts b/apps/web/app/hooks/features/useEmployee.ts index 916877137..4ca1d9037 100644 --- a/apps/web/app/hooks/features/useEmployee.ts +++ b/apps/web/app/hooks/features/useEmployee.ts @@ -6,11 +6,13 @@ import { useRecoilState } from 'recoil'; import { useQuery } from '../useQuery'; import { useAuthenticateUser } from './useAuthenticateUser'; import { IUpdateEmployee } from '@app/interfaces'; +import { useFirstLoad } from '../useFirstLoad'; export const useEmployee = () => { const { user } = useAuthenticateUser(); const [workingEmployees, setWorkingEmployees] = useRecoilState(workingEmployeesState); const [workingEmployeesEmail, setWorkingEmployeesEmail] = useRecoilState(workingEmployeesEmailState); + const { firstLoad, firstLoadData: firstLoadDataEmployee } = useFirstLoad(); const { queryCall: getWorkingEmployeeQueryCall, loading: getWorkingEmployeeLoading } = useQuery(getWorkingEmployeesAPI); @@ -30,10 +32,13 @@ export const useEmployee = () => { }, [getWorkingEmployeeQueryCall, setWorkingEmployees, setWorkingEmployeesEmail, user]); useEffect(() => { - getWorkingEmployee(); - }, [getWorkingEmployee]); + if (firstLoad) { + getWorkingEmployee(); + } + }, [getWorkingEmployee, firstLoad]); return { + firstLoadDataEmployee, getWorkingEmployeeQueryCall, getWorkingEmployeeLoading, workingEmployees, @@ -41,12 +46,10 @@ export const useEmployee = () => { }; }; - export const useEmployeeUpdate = () => { const { queryCall: employeeUpdateQuery, loading: isLoading } = useQuery(updateEmployeeAPI); - const updateEmployee = useCallback(({ id, data - }: { id: string, data: IUpdateEmployee }) => { + const updateEmployee = useCallback(({ id, data }: { id: string; data: IUpdateEmployee }) => { employeeUpdateQuery({ id, data }) .then((res) => res.data) .catch((error) => { @@ -54,5 +57,5 @@ export const useEmployeeUpdate = () => { }); }, []); - return { updateEmployee, isLoading } -} + return { updateEmployee, isLoading }; +}; diff --git a/apps/web/app/hooks/features/useTeamInvitations.ts b/apps/web/app/hooks/features/useTeamInvitations.ts index 47a3bda8c..5c7b16585 100644 --- a/apps/web/app/hooks/features/useTeamInvitations.ts +++ b/apps/web/app/hooks/features/useTeamInvitations.ts @@ -107,7 +107,7 @@ export function useTeamInvitations() { const resendTeamInvitation = useCallback( (invitationId: string) => { - resendInviteQueryCall(invitationId); + return resendInviteQueryCall(invitationId); }, [resendInviteQueryCall] ); diff --git a/apps/web/app/hooks/features/useTeamTasks.ts b/apps/web/app/hooks/features/useTeamTasks.ts index d14d69393..a60f7d6c4 100644 --- a/apps/web/app/hooks/features/useTeamTasks.ts +++ b/apps/web/app/hooks/features/useTeamTasks.ts @@ -15,14 +15,18 @@ import { updateTaskAPI, deleteEmployeeFromTasksAPI, getTasksByIdAPI, - getTasksByEmployeeIdAPI + getTasksByEmployeeIdAPI, + getAllDayPlansAPI, + getMyDailyPlansAPI } from '@app/services/client/api'; import { activeTeamState, activeTeamTaskId, + dailyPlanListState, detailedTaskState, // employeeTasksState, memberActiveTaskIdState, + myDailyPlanListState, userState } from '@app/stores'; import { activeTeamTaskState, tasksByTeamState, tasksFetchingState, teamTasksState } from '@app/stores'; @@ -59,6 +63,9 @@ export function useTeamTasks() { const { firstLoad, firstLoadData: firstLoadTasksData } = useFirstLoad(); + const setDailyPlan = useSetRecoilState(dailyPlanListState); + const setMyDailyPlans = useSetRecoilState(myDailyPlanListState); + // Queries hooks const { queryCall, loading, loadingRef } = useQuery(getTeamTasksAPI); const { queryCall: getTasksByIdQueryCall, loading: getTasksByIdLoading } = useQuery(getTasksByIdAPI); @@ -71,9 +78,30 @@ export function useTeamTasks() { const { queryCall: updateQueryCall, loading: updateLoading } = useQuery(updateTaskAPI); + const { queryCall: getAllQueryCall } = useQuery(getAllDayPlansAPI); + const { queryCall: getMyDailyPlansQueryCall } = useQuery(getMyDailyPlansAPI); + const { queryCall: deleteEmployeeFromTasksQueryCall, loading: deleteEmployeeFromTasksLoading } = useQuery(deleteEmployeeFromTasksAPI); + const getAllDayPlans = useCallback(async () => { + const response = await getAllQueryCall(); + + if (response.data.items.length) { + const { items, total } = response.data; + setDailyPlan({ items, total }); + } + }, [getAllQueryCall, setDailyPlan]); + + const getMyDailyPlans = useCallback(async () => { + const response = await getMyDailyPlansQueryCall(); + + if (response.data.items.length) { + const { items, total } = response.data; + setMyDailyPlans({ items, total }); + } + }, [getMyDailyPlansQueryCall, setMyDailyPlans]); + const getTaskById = useCallback( (taskId: string) => { tasksRef.current.forEach((task) => { @@ -126,13 +154,16 @@ export function useTeamTasks() { const activeTeamTasks = tasksRef.current.slice().sort((a, b) => a.title.localeCompare(b.title)); if (!isEqual(latestActiveTeamTasks, activeTeamTasks)) { + // Fetch plans with updated task(s) + getMyDailyPlans(); + getAllDayPlans(); setAllTasks(responseTasks); } } else { setAllTasks(responseTasks); } }, - [activeTeamRef, setAllTasks, tasksRef] + [activeTeamRef, getAllDayPlans, getMyDailyPlans, setAllTasks, tasksRef] ); const loadTeamTasksData = useCallback( diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts index 0712b14e9..d94e77b27 100644 --- a/apps/web/app/hooks/features/useTimer.ts +++ b/apps/web/app/hooks/features/useTimer.ts @@ -301,6 +301,16 @@ export function useTimer() { return; }); + promise.catch(() => { + if (taskId.current) { + updateLocalTimerStatus({ + lastTaskId: taskId.current, + runnedDateTime: 0, + running: false + }); + } + }); + /** * Updating the task status to "In Progress" when the timer is started. */ diff --git a/apps/web/app/interfaces/IBaseModel.ts b/apps/web/app/interfaces/IBaseModel.ts index 5adf49218..debe7f323 100644 --- a/apps/web/app/interfaces/IBaseModel.ts +++ b/apps/web/app/interfaces/IBaseModel.ts @@ -5,6 +5,15 @@ export interface IBaseDelete { deletedAt?: Date; } +/** + * @description + * An entity ID. Represents a unique identifier as a string. + * + * @docsCategory Type Definitions + * @docsSubcategory Identifiers + */ +export type ID = string; + export interface IBaseEntity extends IBaseDelete { id?: string; readonly createdAt?: Date; diff --git a/apps/web/app/interfaces/IDailyPlan.ts b/apps/web/app/interfaces/IDailyPlan.ts index 2af0ea650..8eba961df 100644 --- a/apps/web/app/interfaces/IDailyPlan.ts +++ b/apps/web/app/interfaces/IDailyPlan.ts @@ -1,6 +1,6 @@ -import { IBasePerTenantAndOrganizationEntity } from './IBaseModel'; -import { IEmployee, IRelationnalEmployee } from './IEmployee'; -import { IOrganization } from './IOrganization'; +import { IBasePerTenantAndOrganizationEntity, ID } from './IBaseModel'; +import { IRelationnalEmployee } from './IEmployee'; +import { IRelationalOrganizationTeam } from './IOrganizationTeam'; import { ITeamTask } from './ITask'; export interface IDailyPlanBase extends IBasePerTenantAndOrganizationEntity { @@ -10,24 +10,27 @@ export interface IDailyPlanBase extends IBasePerTenantAndOrganizationEntity { } export interface IRemoveTaskFromManyPlans { - employeeId?: IEmployee['id']; - plansIds?: IDailyPlan['id'][]; - organizationId?: IOrganization['id']; + employeeId?: ID; + plansIds?: ID[]; + organizationId?: ID; } -export interface IDailyPlan extends IDailyPlanBase, IRelationnalEmployee { +export interface IDailyPlan extends IDailyPlanBase, IRelationnalEmployee, IRelationalOrganizationTeam { tasks?: ITeamTask[]; } -export interface ICreateDailyPlan extends IDailyPlanBase, IRelationnalEmployee { - taskId?: ITeamTask['id']; +export interface ICreateDailyPlan extends IDailyPlanBase, IRelationnalEmployee, IRelationalOrganizationTeam { + taskId?: ID; } -export interface IUpdateDailyPlan extends Partial, Pick { } +export interface IUpdateDailyPlan + extends Partial, + Pick, + Partial> {} export interface IDailyPlanTasksUpdate extends Pick, - IBasePerTenantAndOrganizationEntity { } + IBasePerTenantAndOrganizationEntity {} export enum DailyPlanStatusEnum { OPEN = 'open', diff --git a/apps/web/app/interfaces/IOrganizationTeam.ts b/apps/web/app/interfaces/IOrganizationTeam.ts index fdb5e455a..42256c24f 100644 --- a/apps/web/app/interfaces/IOrganizationTeam.ts +++ b/apps/web/app/interfaces/IOrganizationTeam.ts @@ -74,6 +74,11 @@ export interface IOrganizationTeamList { projects?: IProject[]; } +export interface IRelationalOrganizationTeam { + organizationTeam?: IOrganizationTeam; + organizationTeamId?: IOrganizationTeam['id']; +} + export type IOrganizationTeamWithMStatus = IOrganizationTeamList; export interface OT_Member { diff --git a/apps/web/components/shared/invite/invite-modal.tsx b/apps/web/components/shared/invite/invite-modal.tsx index 09b601351..21dc0c7b6 100644 --- a/apps/web/components/shared/invite/invite-modal.tsx +++ b/apps/web/components/shared/invite/invite-modal.tsx @@ -7,6 +7,7 @@ import React, { useState } from 'react'; import { IInvite, IInviteProps } from '../../../app/interfaces/hooks'; import { UserOutlineIcon } from 'assets/svg'; import { useTranslations } from 'next-intl'; +import { useToast } from '@components/ui/use-toast'; const initalValues: IInvite = { email: '', @@ -14,9 +15,15 @@ const initalValues: IInvite = { }; const InviteModal = ({ isOpen, Fragment, closeModal }: IInviteProps) => { const [formData, setFormData] = useState(initalValues); - const { inviteUser, inviteLoading } = useTeamInvitations(); + const { inviteUser, inviteLoading, teamInvitations, resendTeamInvitation, resendInviteLoading } = + useTeamInvitations(); + const [errors, setErrors] = useState({}); const t = useTranslations(); + const { toast } = useToast(); + + const isLoading = inviteLoading || resendInviteLoading; + const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setErrors((er) => { @@ -30,14 +37,44 @@ const InviteModal = ({ isOpen, Fragment, closeModal }: IInviteProps) => { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); + const existingInvitation = teamInvitations.find((invitation) => invitation.email === formData.email); + + if (existingInvitation) { + resendTeamInvitation(existingInvitation.id).then(() => { + closeModal(); + + toast({ + variant: 'default', + title: t('common.INVITATION_SENT'), + description: t('common.INVITATION_SENT_TO_USER', { email: formData.email }), + duration: 5 * 1000 + }); + }); + return; + } + inviteUser(formData.email, formData.name) - .then(() => { + .then((data) => { setFormData(initalValues); closeModal(); + toast({ + variant: 'default', + title: t('common.INVITATION_SENT'), + description: t('common.INVITATION_SENT_TO_USER', { email: formData.email }), + duration: 5 * 1000 + }); }) .catch((err: AxiosError) => { if (err.response?.status === 400) { - setErrors((err.response?.data as any)?.errors || {}); + const data = err.response?.data as any; + + if ('errors' in data) { + setErrors(data.errors || {}); + } + + if ('message' in data && Array.isArray(data.message)) { + setErrors({ email: data.message[0] }); + } } }); }; @@ -109,10 +146,10 @@ const InviteModal = ({ isOpen, Fragment, closeModal }: IInviteProps) => { + + + + + + {noResultsText} + + {items.map((item) => ( + handleSelect(itemToValue(item))} + > + {itemToString(item)} + + + ))} + + + + + + ) +} diff --git a/apps/web/lib/components/custom-select/index.tsx b/apps/web/lib/components/custom-select/index.tsx index 0e56528b7..0a40d206e 100644 --- a/apps/web/lib/components/custom-select/index.tsx +++ b/apps/web/lib/components/custom-select/index.tsx @@ -1,96 +1,2 @@ -import { Button } from '@components/ui/button'; -import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; -import { cn } from 'lib/utils'; -import { useEffect, useState } from 'react'; -import { MdOutlineKeyboardArrowDown } from 'react-icons/md'; - -interface SelectItemsProps { - items: T[]; - onValueChange?: (value: T) => void; - itemToString: (item: T) => string; - itemId: (item: T) => string; - triggerClassName?: string; - popoverClassName?: string; - renderItem?: (item: T, onClick: () => void) => JSX.Element; - defaultValue?: T; -} - -export function SelectItems({ - items, - onValueChange, - itemToString, - itemId, - triggerClassName = '', - popoverClassName = '', - renderItem, - defaultValue -}: SelectItemsProps) { - const [selectedItem, setSelectedItem] = useState(null); - const [isPopoverOpen, setPopoverOpen] = useState(false); - - const onClick = (item: T) => { - setSelectedItem(item); - setPopoverOpen(false); - if (onValueChange) { - onValueChange(item); - } - }; - - useEffect(() => { - if (defaultValue) { - setSelectedItem(defaultValue); - if (onValueChange) { - onValueChange(defaultValue); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultValue]); - - return ( - - - - - -
- {items.map((item) => - renderItem ? ( - renderItem(item, () => onClick(item)) - ) : ( - onClick(item)} - key={itemId(item)} - className="truncate hover:cursor-pointer hover:bg-slate-50 w-full text-[13px] hover:rounded-lg p-1 hover:font-normal dark:text-white dark:hover:bg-primary" - style={{ textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }} - > - {itemToString(item)} - - ) - )} -
-
-
- ); -} +export * from './multi-select'; +export * from './select-items' diff --git a/apps/web/lib/components/custom-select/multi-select.tsx b/apps/web/lib/components/custom-select/multi-select.tsx new file mode 100644 index 000000000..6b87b8084 --- /dev/null +++ b/apps/web/lib/components/custom-select/multi-select.tsx @@ -0,0 +1,153 @@ +import { Button } from '@components/ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; +import { cn } from 'lib/utils'; +import { useEffect, useState, useRef } from 'react'; +import { MdOutlineKeyboardArrowDown, MdClose } from 'react-icons/md'; + +interface MultiSelectProps { + items: T[]; + onValueChange?: (value: T | T[] | null) => void; + itemToString: (item: T) => string; + itemId: (item: T) => string; + triggerClassName?: string; + popoverClassName?: string; + renderItem?: (item: T, onClick: () => void, isSelected: boolean) => JSX.Element; + defaultValue?: T | T[]; + multiSelect?: boolean; +} + +export function MultiSelect({ + items, + onValueChange, + itemToString, + itemId, + triggerClassName = '', + popoverClassName = '', + renderItem, + defaultValue, + multiSelect = false, +}: MultiSelectProps) { + const [selectedItems, setSelectedItems] = useState(Array.isArray(defaultValue) ? defaultValue : defaultValue ? [defaultValue] : []); + const [isPopoverOpen, setPopoverOpen] = useState(false); + const [popoverWidth, setPopoverWidth] = useState(null); + const triggerRef = useRef(null); + + const onClick = (item: T) => { + let newSelectedItems: T[]; + if (multiSelect) { + if (selectedItems.some((selectedItem) => itemId(selectedItem) === itemId(item))) { + newSelectedItems = selectedItems.filter((selectedItem) => itemId(selectedItem) !== itemId(item)); + } else { + newSelectedItems = [...selectedItems, item]; + } + } else { + newSelectedItems = [item]; + setPopoverOpen(false); + } + setSelectedItems(newSelectedItems); + if (onValueChange) { + onValueChange(multiSelect ? newSelectedItems : newSelectedItems[0]); + } + }; + + const removeItem = (item: T) => { + const newSelectedItems = selectedItems.filter((selectedItem) => itemId(selectedItem) !== itemId(item)); + setSelectedItems(newSelectedItems); + if (onValueChange) { + onValueChange(multiSelect ? newSelectedItems : newSelectedItems.length > 0 ? newSelectedItems[0] : null); + } + }; + + useEffect(() => { + const initialItems = Array.isArray(defaultValue) ? defaultValue : defaultValue ? [defaultValue] : []; + setSelectedItems(initialItems); + if (onValueChange) { + onValueChange(multiSelect ? initialItems : initialItems[0] || null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultValue]); + + useEffect(() => { + if (triggerRef.current) { + setPopoverWidth(triggerRef.current.offsetWidth); + } + }, [triggerRef.current]); + + return ( +
+ + + + + +
+ {items.map((item) => { + const isSelected = selectedItems.some((selectedItem) => itemId(selectedItem) === itemId(item)); + return renderItem ? ( + renderItem(item, () => onClick(item), isSelected) + ) : ( + onClick(item)} + key={itemId(item)} + className={cn( + 'truncate hover:cursor-pointer hover:bg-slate-50 w-full text-[13px] hover:rounded-lg p-1 hover:font-normal dark:text-white dark:hover:bg-primary', + isSelected && 'font-semibold bg-slate-100 dark:bg-primary-light' + )} + style={{ textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }} + > + {itemToString(item)} + + ); + })} +
+
+
+ {selectedItems.length > 0 && ( +
+ {selectedItems.map((item) => ( +
+ {itemToString(item)} + +
+ ))} +
+ )} +
+ ); +} diff --git a/apps/web/lib/components/custom-select/select-items.tsx b/apps/web/lib/components/custom-select/select-items.tsx new file mode 100644 index 000000000..0e56528b7 --- /dev/null +++ b/apps/web/lib/components/custom-select/select-items.tsx @@ -0,0 +1,96 @@ +import { Button } from '@components/ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; +import { cn } from 'lib/utils'; +import { useEffect, useState } from 'react'; +import { MdOutlineKeyboardArrowDown } from 'react-icons/md'; + +interface SelectItemsProps { + items: T[]; + onValueChange?: (value: T) => void; + itemToString: (item: T) => string; + itemId: (item: T) => string; + triggerClassName?: string; + popoverClassName?: string; + renderItem?: (item: T, onClick: () => void) => JSX.Element; + defaultValue?: T; +} + +export function SelectItems({ + items, + onValueChange, + itemToString, + itemId, + triggerClassName = '', + popoverClassName = '', + renderItem, + defaultValue +}: SelectItemsProps) { + const [selectedItem, setSelectedItem] = useState(null); + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const onClick = (item: T) => { + setSelectedItem(item); + setPopoverOpen(false); + if (onValueChange) { + onValueChange(item); + } + }; + + useEffect(() => { + if (defaultValue) { + setSelectedItem(defaultValue); + if (onValueChange) { + onValueChange(defaultValue); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultValue]); + + return ( + + + + + +
+ {items.map((item) => + renderItem ? ( + renderItem(item, () => onClick(item)) + ) : ( + onClick(item)} + key={itemId(item)} + className="truncate hover:cursor-pointer hover:bg-slate-50 w-full text-[13px] hover:rounded-lg p-1 hover:font-normal dark:text-white dark:hover:bg-primary" + style={{ textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }} + > + {itemToString(item)} + + ) + )} +
+
+
+ ); +} diff --git a/apps/web/lib/components/index.ts b/apps/web/lib/components/index.ts index 7c3a6edda..c9f001e4a 100644 --- a/apps/web/lib/components/index.ts +++ b/apps/web/lib/components/index.ts @@ -22,7 +22,7 @@ export * from './color-picker'; export * from './no-data'; export * from './pagination'; export * from './time-picker' -export * from './custom-select' +export * from './custom-select/select-items' export * from './inputs/input'; export * from './inputs/auth-code-input'; diff --git a/apps/web/lib/components/typography.tsx b/apps/web/lib/components/typography.tsx index 8b737cf94..1bf8fbd6a 100644 --- a/apps/web/lib/components/typography.tsx +++ b/apps/web/lib/components/typography.tsx @@ -7,12 +7,25 @@ import { IVariant } from './types'; type Props = PropsWithChildren; /** - *

+ *

*/ -export const Text = ({ children, ...props }: Props & React.ComponentPropsWithRef<'p'>) => { - return

{children}

; +export const Text = ({ children, ...props }: Props & React.ComponentPropsWithRef<'div'>) => { + return
{children}
; }; +/** + *

+ */ +Text.P = forwardRef>(({ children, ...props }, ref) => { + return ( +

+ {children} +

+ ); +}); + +Text.P.displayName = 'TextParagraph'; + /** * */ diff --git a/apps/web/lib/features/daily-plan/active-task-handler-modal.tsx b/apps/web/lib/features/daily-plan/active-task-handler-modal.tsx new file mode 100644 index 000000000..ce96c4ad9 --- /dev/null +++ b/apps/web/lib/features/daily-plan/active-task-handler-modal.tsx @@ -0,0 +1,162 @@ +import { ITeamTask } from '@app/interfaces'; +import { Modal, Card, Text } from 'lib/components'; +import { Button } from '@components/ui/button'; +import { clsxm } from '@app/utils'; +import { useTranslations } from 'next-intl'; +import { useCallback, useMemo, useState } from 'react'; +import { useDailyPlan, useTeamTasks, useTimerView } from '@app/hooks'; +import { RadioGroup } from '@headlessui/react'; +import { DEFAULT_PLANNED_TASK_ID } from '@app/constants'; + +/** + * A Modal that suggests the user to change the active task to a task from the today's plan. + * + * @param {Object} props - The props Object + * @param {boolean} props.open - If true open the modal otherwise close the modal + * @param {() => void} props.closeModal - A function to close the modal + * @param {ITeamTask} props.defaultPlannedTask - The default task from the Today's plan + * + * @returns {JSX.Element} The modal element + */ +export function ActiveTaskHandlerModal({ + open, + closeModal, + defaultPlannedTask +}: { + open: boolean; + closeModal: () => void; + defaultPlannedTask: ITeamTask; +}) { + const t = useTranslations(); + const { startTimer, hasPlan: todayPlan } = useTimerView(); + const { activeTeamTask, setActiveTask } = useTeamTasks(); + const { addTaskToPlan } = useDailyPlan(); + + const [selectedOption, setSelectedOption] = useState(); + + const handleCloseModal = useCallback(() => { + closeModal(); + startTimer(); + }, [closeModal, startTimer]); + + const options = useMemo( + () => [ + { + id: 0, + description: 'Change to planned default task', + action: async () => { + if (defaultPlannedTask) { + setActiveTask(defaultPlannedTask); + } + } + }, + { + id: 1, + description: 'Add the task as default to the plan', + action: async () => { + try { + if (todayPlan && todayPlan.id && activeTeamTask) { + await addTaskToPlan({ taskId: activeTeamTask.id }, todayPlan.id); + } + + activeTeamTask && + window && + window.localStorage.setItem(DEFAULT_PLANNED_TASK_ID, activeTeamTask.id); + } catch (error) { + console.log(error); + } + } + }, + { + id: 2, + description: 'Add the task AND work on the default task', + action: async () => { + try { + if (todayPlan && todayPlan.id && activeTeamTask) { + await addTaskToPlan({ taskId: activeTeamTask.id }, todayPlan.id); + } + if (defaultPlannedTask) { + setActiveTask(defaultPlannedTask); + } + } catch (error) { + console.log(error); + } + } + } + ], + [activeTeamTask, addTaskToPlan, defaultPlannedTask, setActiveTask, todayPlan] + ); + + const handleSubmit = useCallback(() => { + selectedOption !== undefined && options[selectedOption].action(); + handleCloseModal(); + }, [handleCloseModal, options, selectedOption]); + + return ( + + +
+
+ + {t('dailyPlan.chang_active_task_popup.TITLE')} + + + + {t('dailyPlan.chang_active_task_popup.MESSAGE', { + defaultPlannedTask: defaultPlannedTask.taskNumber, + activeTask: activeTeamTask?.taskNumber + })} + +
+ +
+ + {options.map((option) => { + return ( + + {({ checked }) => ( +
+ + + + {option.description} +
+ )} +
+ ); + })} +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx b/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx index dfcff9138..60f7c6c99 100644 --- a/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx +++ b/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx @@ -1,46 +1,186 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { TASKS_ESTIMATE_HOURS_MODAL_DATE } from '@app/constants'; -import { useMemo, useCallback, useState, useEffect } from 'react'; +import { useMemo, useCallback, useState, useEffect, useRef, Dispatch, SetStateAction } from 'react'; import { PiWarningCircleFill } from 'react-icons/pi'; -import { Card, InputField, Modal, Text, VerticalSeparator } from 'lib/components'; +import { Card, InputField, Modal, SpinnerLoader, Text, Tooltip, VerticalSeparator } from 'lib/components'; import { Button } from '@components/ui/button'; import { useTranslations } from 'next-intl'; -import { useDailyPlan, useTeamTasks, useTimerView } from '@app/hooks'; +import { useAuthenticateUser, useDailyPlan, useModal, useTaskStatus, useTeamTasks, useTimerView } from '@app/hooks'; import { TaskNameInfoDisplay } from '../task/task-displays'; import { TaskEstimate } from '../task/task-estimate'; import { IDailyPlan, ITeamTask } from '@app/interfaces'; import clsx from 'clsx'; import { AddIcon, ThreeCircleOutlineVerticalIcon } from 'assets/svg'; +import { estimatedTotalTime } from '../task/daily-plan'; +import { clsxm } from '@app/utils'; +import { formatIntegerToHour } from '@app/helpers'; +import { DEFAULT_PLANNED_TASK_ID } from '@app/constants'; +import { ActiveTaskHandlerModal } from './active-task-handler-modal'; +import { TaskDetailsModal } from './task-details-modal'; +import { Popover, Transition } from '@headlessui/react'; +import { ScrollArea, ScrollBar } from '@components/ui/scroll-bar'; +import { Cross2Icon } from '@radix-ui/react-icons'; +/** + * A modal that allows user to add task estimation / planned work time, etc. + * + * @param {Object} props - The props Object + * @param {boolean} props.open - If true open the modal otherwise close the modal + * @param {() => void} props.closeModal - A function to close the modal + * @param {IDailyPlan} props.plan - The selected plan + * @param {ITeamTask[]} props.tasks - The list of planned tasks + * @param {boolean} props.isRenderedInSoftFlow - If true use the soft flow logic. + * + * @returns {JSX.Element} The modal element + */ interface IAddTasksEstimationHoursModalProps { closeModal: () => void; isOpen: boolean; plan: IDailyPlan; tasks: ITeamTask[]; + isRenderedInSoftFlow?: boolean; } export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModalProps) { - const { isOpen, closeModal, plan, tasks } = props; + const { isOpen, closeModal, plan, tasks, isRenderedInSoftFlow = true } = props; + const { + isOpen: isActiveTaskHandlerModalOpen, + closeModal: closeActiveTaskHandlerModal, + openModal: openActiveTaskHandlerModal + } = useModal(); const t = useTranslations(); - const { updateDailyPlan } = useDailyPlan(); + const { updateDailyPlan, myDailyPlans } = useDailyPlan(); const { startTimer } = useTimerView(); const { activeTeam, activeTeamTask, setActiveTask } = useTeamTasks(); - - const [workTimePlanned, setworkTimePlanned] = useState(plan.workTimePlanned); + const [showSearchInput, setShowSearchInput] = useState(false); + const [workTimePlanned, setWorkTimePlanned] = useState(plan.workTimePlanned); const currentDate = useMemo(() => new Date().toISOString().split('T')[0], []); const requirePlan = useMemo(() => activeTeam?.requirePlanToTrack, [activeTeam?.requirePlanToTrack]); + const tasksEstimationTimes = useMemo(() => estimatedTotalTime(plan.tasks).timesEstimated / 3600, [plan.tasks]); + const [warning, setWarning] = useState(''); + const [loading, setLoading] = useState(false); + const [defaultTask, setDefaultTask] = useState(null); + const isActiveTaskPlanned = useMemo( + () => plan.tasks?.some((task) => task.id == activeTeamTask?.id), + [activeTeamTask?.id, plan.tasks] + ); + + const canStartWorking = useMemo(() => { + const isTodayPlan = + new Date(Date.now()).toLocaleDateString('en') == new Date(plan.date).toLocaleDateString('en'); + + return isTodayPlan; + // Can add others conditions + }, [plan.date]); const handleCloseModal = useCallback(() => { - localStorage.setItem(TASKS_ESTIMATE_HOURS_MODAL_DATE, currentDate); + if (canStartWorking) { + localStorage.setItem(TASKS_ESTIMATE_HOURS_MODAL_DATE, currentDate); + } closeModal(); - startTimer(); - }, [closeModal, currentDate, startTimer]); - - const handleSubmit = useCallback(() => { - updateDailyPlan({ workTimePlanned }, plan.id ?? ''); + }, [canStartWorking, closeModal, currentDate]); + /** + * The function that close the Planned tasks modal when the user ignores the modal (Today's plan) + */ + const closeModalAndStartTimer = useCallback(() => { handleCloseModal(); - }, [handleCloseModal, plan.id, updateDailyPlan, workTimePlanned]); + if (canStartWorking) { + startTimer(); + } + }, [canStartWorking, handleCloseModal, startTimer]); + + /** + * The function that opens the Change task modal if conditions are met (or start the timer) + */ + const handleChangeActiveTask = useCallback(() => { + if (isActiveTaskPlanned) { + if (defaultTask?.id !== activeTeamTask?.id) { + setActiveTask(defaultTask); + } + + if (!isRenderedInSoftFlow) { + handleCloseModal(); + } + startTimer(); + } else { + openActiveTaskHandlerModal(); + } + }, [ + activeTeamTask?.id, + defaultTask, + handleCloseModal, + isActiveTaskPlanned, + openActiveTaskHandlerModal, + isRenderedInSoftFlow, + setActiveTask, + startTimer + ]); + + /** + * The function which is called when the user clicks on the 'Start working' button + */ + const handleSubmit = useCallback(async () => { + try { + setLoading(true); + + // Update the plan work time only if the user changed it + plan.workTimePlanned !== workTimePlanned && (await updateDailyPlan({ workTimePlanned }, plan.id ?? '')); + + if (canStartWorking) { + handleChangeActiveTask(); + + if (isRenderedInSoftFlow) { + handleCloseModal(); + } + } + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }, [ + canStartWorking, + handleChangeActiveTask, + handleCloseModal, + plan.id, + plan.workTimePlanned, + isRenderedInSoftFlow, + updateDailyPlan, + workTimePlanned + ]); + + /** + * The function that handles warning messages for the + * difference of time (planned work / total estimated) + */ + const checkPlannedAndEstimateTimeDiff = useCallback(() => { + if (workTimePlanned) { + if (workTimePlanned > tasksEstimationTimes) { + setWarning(t('dailyPlan.planned_tasks_popup.warning.PLAN_MORE_TASKS')); + } else { + setWarning(t('dailyPlan.planned_tasks_popup.warning.OPTIMIZE_PLAN')); + } + } else { + setWarning(t('dailyPlan.planned_tasks_popup.warning.PLANNED_TIME')); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tasksEstimationTimes, workTimePlanned]); + + // Handle warning messages + useEffect(() => { + if (!workTimePlanned || workTimePlanned <= 0) { + setWarning(t('dailyPlan.planned_tasks_popup.warning.PLANNED_TIME')); + } else if (plan.tasks?.find((task) => !task.estimate)) { + setWarning(t('dailyPlan.planned_tasks_popup.warning.TASKS_ESTIMATION')); + } else if (Math.abs(workTimePlanned - tasksEstimationTimes) > 1) { + checkPlannedAndEstimateTimeDiff(); + } else { + setWarning(''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [workTimePlanned, tasksEstimationTimes, plan.tasks, myDailyPlans]); // Put tasks without estimates at the top of the list const sortedTasks = useMemo( @@ -62,113 +202,653 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa if (!sortedTasks.find((task) => task.id == activeTeamTask?.id)) { [...sortedTasks].forEach((task) => { if (task.estimate !== null && task.estimate > 0) { - isOpen && setActiveTask(task); + if (isOpen) { + setDefaultTask(task); + window && window.localStorage.setItem(DEFAULT_PLANNED_TASK_ID, task.id); + } } }); + } else { + if (isOpen && activeTeamTask) { + setDefaultTask(activeTeamTask); + window && window.localStorage.setItem(DEFAULT_PLANNED_TASK_ID, activeTeamTask.id); + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen]); + }, [isOpen, tasks]); - return ( - - -
-
- - {t('timer.todayPlanSettings.TITLE')} - -
- - {t('timer.todayPlanSettings.WORK_TIME_PLANNED')} * - -
- setworkTimePlanned(parseFloat(e.target.value))} - required - noWrapper - min={0} - value={workTimePlanned} - defaultValue={plan.workTimePlanned ?? 0} - /> -
- -
-
+ // Update the working planned time + useEffect(() => { + setWorkTimePlanned(plan.workTimePlanned); + }, [plan]); + + const content = ( +
+
+ {isRenderedInSoftFlow && ( + + {t('timer.todayPlanSettings.TITLE')} + + )} + + {showSearchInput ? ( + + ) : ( +
+ + {t('timer.todayPlanSettings.WORK_TIME_PLANNED')} * + +
+ setWorkTimePlanned(parseFloat(e.target.value))} + required + noWrapper + min={0} + value={workTimePlanned} + defaultValue={plan.workTimePlanned ?? 0} + /> +
-
-
- - {t('timer.todayPlanSettings.TASKS_WITH_NO_ESTIMATIONS')}{' '} +
+ )} + +
+
+
+
+
+ {t('task.TITLE_PLURAL')} * - -
- {sortedTasks.map((task, index) => ( - - ))} +
+
+ {t('dailyPlan.TOTAL_ESTIMATED')} : + {formatIntegerToHour(tasksEstimationTimes)}
-
- -

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

+
+ +
    + {sortedTasks.map((task, index) => ( + + ))} +
+ +
+
+ {warning && ( + <> + + {warning} + + )} +
-
- +
+
+ + +
+
+
+ ); + + return ( + <> + {isRenderedInSoftFlow ? ( + + + {content} + + + ) : ( + content + )} + + {defaultTask && ( + { + if (!isRenderedInSoftFlow) { + handleCloseModal(); + } + closeActiveTaskHandlerModal(); + }} + /> + )} + + ); +} + +/** + * ---------------------------------------------------------------- + * --------- Search / Add / Create task input ----------- + * ---------------------------------------------------------------- + */ + +interface ISearchTaskInputProps { + selectedPlan: IDailyPlan; + setShowSearchInput: Dispatch>; + setDefaultTask: Dispatch>; + defaultTask: ITeamTask | null; +} + +/** + * Search task input + * + * @param {Object} props - The props object + * @param {string} props.selectedPlan - The selected plan + * @param {Dispatch>} props.setShowSearchInput - A setter for (showing / hiding) the input + * @param {Dispatch>} props.setDefaultTask - A function that sets default planned task + * @param {ITeamTask} props.defaultTask - The default planned task + * + * @returns The Search input component + */ +function SearchTaskInput(props: ISearchTaskInputProps) { + const { selectedPlan, setShowSearchInput, defaultTask, setDefaultTask } = props; + const { tasks: teamTasks, createTask } = useTeamTasks(); + const { taskStatus } = useTaskStatus(); + const [taskName, setTaskName] = useState(''); + const [tasks, setTasks] = useState([]); + const [createTaskLoading, setCreateTaskLoading] = useState(false); + const [isSearchInputFocused, setIsSearchInputFocused] = useState(false); + const t = useTranslations(); + + // The ref for the popover button (rendered as an input) + const searchInputRef = useRef(null); + + const isTaskPlanned = useCallback( + (taskId: string) => { + return selectedPlan?.tasks?.some((task) => task.id == taskId); + }, + [selectedPlan.tasks] + ); + + useEffect(() => { + setTasks( + teamTasks + .filter((task) => task.title.toLowerCase().includes(taskName.toLowerCase())) + // Put the unplanned tasks at the top of the list. + .sort((task1, task2) => { + if (isTaskPlanned(task1.id) && !isTaskPlanned(task2.id)) { + return 1; + } else if (!isTaskPlanned(task1.id) && isTaskPlanned(task2.id)) { + return -1; + } else { + return 0; + } + }) + ); + }, [isTaskPlanned, selectedPlan.tasks, taskName, teamTasks]); + + const handleCreateTask = useCallback(async () => { + try { + setCreateTaskLoading(true); + if (taskName.trim().length < 5) return; + await createTask({ + taskName: taskName.trim(), + status: taskStatus[0].name, + taskStatusId: taskStatus[0].id, + issueType: 'Bug' // TODO: Let the user choose the issue type + }); + } catch (error) { + console.log(error); + } finally { + setCreateTaskLoading(false); + } + }, [createTask, taskName, taskStatus]); + + /** + * Focus on the search input when the popover is mounted. + */ + useEffect(() => { + searchInputRef.current?.focus(); + }, []); + + return ( + +
+ Select or create task for the plan +
+ setTaskName(e.target.value)} + onFocus={() => setIsSearchInputFocused(true)} + value={taskName} + /> + +
+
+ + + {tasks.length ? ( + + +
    + {tasks.map((task, index) => ( +
  • + +
  • + ))} +
+ +
+
+ ) : ( + -
-
- - + + )} + + ); } +/** + * ---------------------------------------------------------------- + * -------------------- TASK CARD ----------------------- + * ---------------------------------------------------------------- + */ + interface ITaskCardProps { task: ITeamTask; + setDefaultTask: Dispatch>; + isDefaultTask: boolean; + plan: IDailyPlan; + viewListMode?: 'planned' | 'searched'; } -function TaskCard({ task }: ITaskCardProps) { - const { setActiveTask, activeTeamTask } = useTeamTasks(); +function TaskCard(props: ITaskCardProps) { + const { task, plan, viewListMode = 'planned', isDefaultTask, setDefaultTask } = props; + const { getTaskById } = useTeamTasks(); + const { addTaskToPlan } = useDailyPlan(); + const [addToPlanLoading, setAddToPlanLoading] = useState(false); + const isTaskRenderedInTodayPlan = + new Date(Date.now()).toLocaleDateString('en') == new Date(plan.date).toLocaleDateString('en'); + const { + isOpen: isTaskDetailsModalOpen, + closeModal: closeTaskDetailsModal, + openModal: openTaskDetailsModal + } = useModal(); + + const handleOpenTaskDetailsModal = useCallback(() => { + // Update the detailed task state + getTaskById(task.id); + openTaskDetailsModal(); + }, [getTaskById, openTaskDetailsModal, task.id]); + + const t = useTranslations(); + + /** + * The function that adds the task to the selected plan + */ + const handleAddTask = useCallback(async () => { + try { + setAddToPlanLoading(true); + + if (plan.id) await addTaskToPlan({ taskId: task.id }, plan.id); + } catch (error) { + console.log(error); + } finally { + setAddToPlanLoading(false); + } + }, [addTaskToPlan, plan.id, task.id]); return (