diff --git a/front/src/AppNavbar.tsx b/front/src/AppNavbar.tsx index 0bdf5e2db..d89ffa8a6 100644 --- a/front/src/AppNavbar.tsx +++ b/front/src/AppNavbar.tsx @@ -1,5 +1,6 @@ import { useLocation, useNavigate } from 'react-router-dom'; +import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { Favorites } from '@/favorites/components/Favorites'; import { SettingsNavbar } from '@/settings/components/SettingsNavbar'; @@ -26,6 +27,7 @@ export function AppNavbar() { const navigate = useNavigate(); const isInSubMenu = useIsSubMenuNavbarDisplayed(); + const { currentUserDueTaskCount } = useCurrentUserTaskCount(); return ( <> @@ -54,6 +56,7 @@ export function AppNavbar() { to="/tasks" active={currentPath === '/tasks'} Icon={IconCheckbox} + count={currentUserDueTaskCount} /> diff --git a/front/src/modules/activities/tasks/components/TaskGroups.tsx b/front/src/modules/activities/tasks/components/TaskGroups.tsx index fcdd37b59..ca010fdf5 100644 --- a/front/src/modules/activities/tasks/components/TaskGroups.tsx +++ b/front/src/modules/activities/tasks/components/TaskGroups.tsx @@ -68,7 +68,7 @@ export function TaskGroups({ entity, showAddButton }: OwnProps) { ); if ( - (activeTabId === 'to-do' && + (activeTabId !== 'done' && todayOrPreviousTasks?.length === 0 && upcomingTasks?.length === 0 && unscheduledTasks?.length === 0) || diff --git a/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts b/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts new file mode 100644 index 000000000..d9d4dcf71 --- /dev/null +++ b/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts @@ -0,0 +1,44 @@ +import { DateTime } from 'luxon'; +import { useRecoilState } from 'recoil'; + +import { currentUserState } from '@/auth/states/currentUserState'; +import { FilterOperand } from '@/ui/view-bar/types/FilterOperand'; +import { turnFilterIntoWhereClause } from '@/ui/view-bar/utils/turnFilterIntoWhereClause'; +import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql'; +import { parseDate } from '~/utils/date-utils'; + +export function useCurrentUserTaskCount() { + const [currentUser] = useRecoilState(currentUserState); + + const { data } = useGetActivitiesQuery({ + variables: { + where: { + type: { equals: ActivityType.Task }, + completedAt: { equals: null }, + ...(currentUser + ? turnFilterIntoWhereClause({ + key: 'assigneeId', + type: 'entity', + value: currentUser.id, + operand: FilterOperand.Is, + displayValue: currentUser.displayName, + displayAvatarUrl: currentUser.avatarUrl ?? undefined, + }) + : {}), + }, + }, + }); + + const currentUserDueTaskCount = data?.findManyActivities.filter((task) => { + if (!task.dueAt) { + return false; + } + const dueDate = parseDate(task.dueAt).toJSDate(); + const today = DateTime.now().endOf('day').toJSDate(); + return dueDate <= today; + }).length; + + return { + currentUserDueTaskCount, + }; +} diff --git a/front/src/modules/activities/tasks/hooks/useInitializeTasksFilters.ts b/front/src/modules/activities/tasks/hooks/useInitializeTasksFilters.ts deleted file mode 100644 index 323d25cd1..000000000 --- a/front/src/modules/activities/tasks/hooks/useInitializeTasksFilters.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from 'react'; - -import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; -import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState'; -import { FilterDefinition } from '@/ui/view-bar/types/FilterDefinition'; - -import { TasksRecoilScopeContext } from '../../states/recoil-scope-contexts/TasksRecoilScopeContext'; - -export function useInitializeTasksFilters({ - availableFilters, -}: { - availableFilters: FilterDefinition[]; -}) { - const [, setAvailableFilters] = useRecoilScopedState( - availableFiltersScopedState, - TasksRecoilScopeContext, - ); - - useEffect(() => { - setAvailableFilters(availableFilters); - }, [setAvailableFilters, availableFilters]); -} diff --git a/front/src/modules/activities/tasks/hooks/useTasks.ts b/front/src/modules/activities/tasks/hooks/useTasks.ts index b3d4f2b12..ac7b43d1b 100644 --- a/front/src/modules/activities/tasks/hooks/useTasks.ts +++ b/front/src/modules/activities/tasks/hooks/useTasks.ts @@ -1,47 +1,19 @@ -import { useEffect } from 'react'; import { DateTime } from 'luxon'; -import { useRecoilState } from 'recoil'; import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext'; -import { useInitializeTasksFilters } from '@/activities/tasks/hooks/useInitializeTasksFilters'; import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity'; -import { currentUserState } from '@/auth/states/currentUserState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState'; -import { FilterOperand } from '@/ui/view-bar/types/FilterOperand'; import { turnFilterIntoWhereClause } from '@/ui/view-bar/utils/turnFilterIntoWhereClause'; import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql'; -import { tasksFilters } from '~/pages/tasks/tasks-filters'; import { parseDate } from '~/utils/date-utils'; export function useTasks(entity?: ActivityTargetableEntity) { - useInitializeTasksFilters({ - availableFilters: tasksFilters, - }); - - const [filters, setFilters] = useRecoilScopedState( + const [filters] = useRecoilScopedState( filtersScopedState, TasksRecoilScopeContext, ); - // If there is no filter, we set the default filter to the current user - const [currentUser] = useRecoilState(currentUserState); - - useEffect(() => { - if (currentUser && !filters.length && !entity) { - setFilters([ - { - key: 'assigneeId', - type: 'entity', - value: currentUser.id, - operand: FilterOperand.Is, - displayValue: currentUser.displayName, - displayAvatarUrl: currentUser.avatarUrl ?? undefined, - }, - ]); - } - }, [currentUser, filters, setFilters, entity]); - const whereFilters = entity ? { activityTargets: { @@ -68,6 +40,7 @@ export function useTasks(entity?: ActivityTargetableEntity) { ...whereFilters, }, }, + skip: !entity && filters.length === 0, }); const { data: incompleteTaskData } = useGetActivitiesQuery({ @@ -78,6 +51,7 @@ export function useTasks(entity?: ActivityTargetableEntity) { ...whereFilters, }, }, + skip: !entity && filters.length === 0, }); const todayOrPreviousTasks = incompleteTaskData?.findManyActivities.filter( @@ -111,9 +85,9 @@ export function useTasks(entity?: ActivityTargetableEntity) { const completedTasks = completeTasksData?.findManyActivities; return { - todayOrPreviousTasks, - upcomingTasks, - unscheduledTasks, - completedTasks, + todayOrPreviousTasks: todayOrPreviousTasks ?? [], + upcomingTasks: upcomingTasks ?? [], + unscheduledTasks: unscheduledTasks ?? [], + completedTasks: completedTasks ?? [], }; } diff --git a/front/src/modules/companies/table/components/companies-mock-data.ts b/front/src/modules/companies/table/components/companies-mock-data.ts index 534f80056..9a1857e33 100644 --- a/front/src/modules/companies/table/components/companies-mock-data.ts +++ b/front/src/modules/companies/table/components/companies-mock-data.ts @@ -1,4 +1,4 @@ -import { Company, User } from '../../../../generated/graphql'; +import { Company, Favorite, User } from '../../../../generated/graphql'; type MockedCompany = Pick< Company, @@ -25,7 +25,7 @@ type MockedCompany = Pick< | 'firstName' | 'lastName' > | null; -}; +} & { Favorite: Pick | null }; export const mockedCompaniesData: Array = [ { @@ -39,6 +39,7 @@ export const mockedCompaniesData: Array = [ createdAt: '2023-04-26T10:08:54.724515+00:00', address: 'San Francisco, CA', employees: 5000, + Favorite: null, _activityCount: 0, accountOwner: { email: 'charles@test.com', @@ -62,6 +63,7 @@ export const mockedCompaniesData: Array = [ createdAt: '2023-04-26T10:12:42.33625+00:00', address: 'Paris, France', employees: 800, + Favorite: null, _activityCount: 0, accountOwner: null, __typename: 'Company', @@ -77,6 +79,7 @@ export const mockedCompaniesData: Array = [ createdAt: '2023-04-26T10:10:32.530184+00:00', address: 'San Francisco, CA', employees: 8000, + Favorite: null, _activityCount: 0, accountOwner: null, __typename: 'Company', @@ -92,6 +95,7 @@ export const mockedCompaniesData: Array = [ createdAt: '2023-03-21T06:30:25.39474+00:00', address: 'San Francisco, CA', employees: 800, + Favorite: null, _activityCount: 0, accountOwner: null, __typename: 'Company', @@ -107,6 +111,7 @@ export const mockedCompaniesData: Array = [ createdAt: '2023-04-26T10:13:29.712485+00:00', address: 'San Francisco, CA', employees: 400, + Favorite: null, _activityCount: 0, accountOwner: null, __typename: 'Company', diff --git a/front/src/modules/ui/navbar/components/NavItem.tsx b/front/src/modules/ui/navbar/components/NavItem.tsx index 7c1706965..3159e2fc1 100644 --- a/front/src/modules/ui/navbar/components/NavItem.tsx +++ b/front/src/modules/ui/navbar/components/NavItem.tsx @@ -17,6 +17,7 @@ type NavItemProps = { active?: boolean; danger?: boolean; soon?: boolean; + count?: number; }; type StyledItemProps = { @@ -82,6 +83,21 @@ const StyledSoonPill = styled.div` padding-right: ${({ theme }) => theme.spacing(2)}; `; +const StyledItemCount = styled.div` + align-items: center; + background-color: ${({ theme }) => theme.color.blue}; + border-radius: ${({ theme }) => theme.border.radius.rounded}; + color: ${({ theme }) => theme.grayScale.gray0}; + display: flex; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + + height: 16px; + justify-content: center; + margin-left: auto; + width: 16px; +`; + function NavItem({ label, Icon, @@ -90,6 +106,7 @@ function NavItem({ active, danger, soon, + count, }: NavItemProps) { const theme = useTheme(); const navigate = useNavigate(); @@ -120,6 +137,7 @@ function NavItem({ {Icon && } {label} {soon && Soon} + {!!count && {count}} ); } diff --git a/front/src/pages/tasks/Tasks.tsx b/front/src/pages/tasks/Tasks.tsx index 78ae9c6c0..103f9fe6b 100644 --- a/front/src/pages/tasks/Tasks.tsx +++ b/front/src/pages/tasks/Tasks.tsx @@ -14,6 +14,8 @@ import { TopBar } from '@/ui/top-bar/TopBar'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { FilterDropdownButton } from '@/ui/view-bar/components/FilterDropdownButton'; +import { TasksEffect } from './TasksEffect'; + const StyledTasksContainer = styled.div` display: flex; flex: 1; @@ -47,6 +49,7 @@ export function Tasks() { + diff --git a/front/src/pages/tasks/TasksEffect.tsx b/front/src/pages/tasks/TasksEffect.tsx new file mode 100644 index 000000000..113c25537 --- /dev/null +++ b/front/src/pages/tasks/TasksEffect.tsx @@ -0,0 +1,44 @@ +import { useEffect } from 'react'; +import { useRecoilState } from 'recoil'; + +import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext'; +import { currentUserState } from '@/auth/states/currentUserState'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; +import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState'; +import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState'; +import { FilterOperand } from '@/ui/view-bar/types/FilterOperand'; + +import { tasksFilters } from './tasks-filters'; + +export function TasksEffect() { + const [currentUser] = useRecoilState(currentUserState); + const [, setFilters] = useRecoilScopedState( + filtersScopedState, + TasksRecoilScopeContext, + ); + + const [, setAvailableFilters] = useRecoilScopedState( + availableFiltersScopedState, + TasksRecoilScopeContext, + ); + + useEffect(() => { + setAvailableFilters(tasksFilters); + }, [setAvailableFilters]); + + useEffect(() => { + if (currentUser) { + setFilters([ + { + key: 'assigneeId', + type: 'entity', + value: currentUser.id, + operand: FilterOperand.Is, + displayValue: currentUser.displayName, + displayAvatarUrl: currentUser.avatarUrl ?? undefined, + }, + ]); + } + }, [currentUser, setFilters]); + return <>; +} diff --git a/front/src/testing/mock-data/activities.ts b/front/src/testing/mock-data/activities.ts index 867c268ee..b332722cb 100644 --- a/front/src/testing/mock-data/activities.ts +++ b/front/src/testing/mock-data/activities.ts @@ -21,20 +21,14 @@ type MockedActivity = Pick< | 'dueAt' | 'completedAt' > & { - author: { - __typename?: 'User' | undefined; - id: string; - firstName: string; - lastName: string; - displayName: string; - }; - assignee: { - __typename?: 'User' | undefined; - id: string; - firstName: string; - lastName: string; - displayName: string; - }; + author: Pick< + User, + 'id' | 'firstName' | 'lastName' | 'displayName' | 'avatarUrl' + >; + assignee: Pick< + User, + 'id' | 'firstName' | 'lastName' | 'displayName' | 'avatarUrl' + >; comments: Array< Pick & { author: Pick; @@ -52,8 +46,8 @@ type MockedActivity = Pick< | 'companyId' > & { activity: Pick; - person?: Pick; - company?: Pick; + person?: Pick | null; + company?: Pick | null; } >; }; @@ -73,12 +67,14 @@ export const mockedTasks: Array = [ firstName: 'Charles', lastName: 'Test', displayName: 'Charles Test', + avatarUrl: '', }, assignee: { id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', firstName: 'Charles', lastName: 'Test', displayName: 'Charles Test', + avatarUrl: '', }, authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', comments: [], @@ -102,12 +98,14 @@ export const mockedActivities: Array = [ firstName: 'Charles', lastName: 'Test', displayName: 'Charles Test', + avatarUrl: '', }, assignee: { id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', firstName: 'Charles', lastName: 'Test', displayName: 'Charles Test', + avatarUrl: '', }, authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', comments: [], @@ -117,12 +115,13 @@ export const mockedActivities: Array = [ createdAt: '2023-04-26T10:12:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00', personId: null, - companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280', company: { - id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + id: '89bb825c-171e-4bcc-9cf7-43448d6fb280', name: 'Airbnb', domainName: 'airbnb.com', }, + person: null, activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230', activity: { id: '89bb825c-171e-4bcc-9cf7-43448d6fb230', @@ -142,7 +141,8 @@ export const mockedActivities: Array = [ name: 'Aircall', domainName: 'aircall.io', }, - activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb231', + person: null, + activityId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae', activity: { id: '89bb825c-171e-4bcc-9cf7-43448d6fb231', createdAt: new Date().toISOString(), @@ -154,7 +154,7 @@ export const mockedActivities: Array = [ __typename: 'Activity', }, { - id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), title: 'Another note', @@ -167,43 +167,53 @@ export const mockedActivities: Array = [ firstName: 'Charles', lastName: 'Test', displayName: 'Charles Test', + avatarUrl: '', }, assignee: { id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', firstName: 'Charles', lastName: 'Test', displayName: 'Charles Test', + avatarUrl: '', }, authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', comments: [], activityTargets: [ { - id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + id: '89bb825c-171e-4bcc-9cf7-43448d6fb278t', createdAt: '2023-04-26T10:12:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00', personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre person: { id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', displayName: 'Alexandre Test', + avatarUrl: '', }, + company: null, companyId: null, - activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278a', activity: { - id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a', createdAt: '2023-04-26T10:12:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00', }, __typename: 'ActivityTarget', }, { - id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + id: '89bb825c-171e-4bcc-9cf7-43448d6fb279t', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau companyId: null, - activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + company: null, + person: { + id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', + displayName: "Jean d'Eau", + avatarUrl: '', + }, + activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278a', activity: { - id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', + id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, diff --git a/front/src/testing/mock-data/companies.ts b/front/src/testing/mock-data/companies.ts index 4981fc301..a95384d9b 100644 --- a/front/src/testing/mock-data/companies.ts +++ b/front/src/testing/mock-data/companies.ts @@ -1,5 +1,12 @@ import { companiesAvailableColumnDefinitions } from '@/companies/constants/companiesAvailableColumnDefinitions'; -import { Company, User, View, ViewField, ViewType } from '~/generated/graphql'; +import { + Company, + Favorite, + User, + View, + ViewField, + ViewType, +} from '~/generated/graphql'; type MockedCompany = Pick< Company, @@ -26,7 +33,7 @@ type MockedCompany = Pick< | 'firstName' | 'lastName' > | null; -}; +} & { Favorite: Pick | null }; export const mockedCompaniesData: Array = [ { @@ -41,6 +48,7 @@ export const mockedCompaniesData: Array = [ annualRecurringRevenue: 500000, idealCustomerProfile: true, _activityCount: 1, + Favorite: null, accountOwner: { email: 'charles@test.com', displayName: 'Charles Test', @@ -65,6 +73,7 @@ export const mockedCompaniesData: Array = [ idealCustomerProfile: false, _activityCount: 1, accountOwner: null, + Favorite: null, __typename: 'Company', }, { @@ -80,6 +89,7 @@ export const mockedCompaniesData: Array = [ idealCustomerProfile: true, _activityCount: 1, accountOwner: null, + Favorite: null, __typename: 'Company', }, { @@ -95,6 +105,7 @@ export const mockedCompaniesData: Array = [ idealCustomerProfile: false, _activityCount: 0, accountOwner: null, + Favorite: null, __typename: 'Company', }, { @@ -110,6 +121,7 @@ export const mockedCompaniesData: Array = [ idealCustomerProfile: false, _activityCount: 2, accountOwner: null, + Favorite: null, __typename: 'Company', }, { @@ -125,6 +137,7 @@ export const mockedCompaniesData: Array = [ idealCustomerProfile: true, _activityCount: 13, accountOwner: null, + Favorite: null, __typename: 'Company', }, { @@ -140,6 +153,7 @@ export const mockedCompaniesData: Array = [ idealCustomerProfile: true, _activityCount: 1, accountOwner: null, + Favorite: null, __typename: 'Company', }, ]; diff --git a/play.md b/play.md new file mode 100644 index 000000000..7decace75 --- /dev/null +++ b/play.md @@ -0,0 +1,8 @@ +What does this mean? And what does `WorkspaceMember` look like? + +```prisma +model User { + /// @TypeGraphQL.omit(input: true) + workspaceMember WorkspaceMember? +} +``` \ No newline at end of file