diff --git a/assets/images/simple-illustrations/simple-illustration__tag.svg b/assets/images/simple-illustrations/simple-illustration__tag.svg new file mode 100644 index 000000000000..0cac51679a5e --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__tag.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/tag.svg b/assets/images/tag.svg new file mode 100644 index 000000000000..f5e13b8135cb --- /dev/null +++ b/assets/images/tag.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fb99108c7e97..f3499c983378 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -542,7 +542,10 @@ const ROUTES = { route: 'workspace/:policyID/categories/settings', getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, }, - + WORKSPACE_TAGS: { + route: 'workspace/:policyID/tags', + getRoute: (policyID: string) => `workspace/${policyID}/tags` as const, + }, // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index cc7df01524f7..76a9eaa96009 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -214,6 +214,7 @@ const SCREENS = { INVITE: 'Workspace_Invite', INVITE_MESSAGE: 'Workspace_Invite_Message', CATEGORIES: 'Workspace_Categories', + TAGS: 'Workspace_Tags', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 6c6c1b86eee1..d9f46203a703 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -135,6 +135,7 @@ import Twitter from '@assets/images/social-twitter.svg'; import Youtube from '@assets/images/social-youtube.svg'; import Stopwatch from '@assets/images/stopwatch.svg'; import Sync from '@assets/images/sync.svg'; +import Tag from '@assets/images/tag.svg'; import Task from '@assets/images/task.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; @@ -222,6 +223,7 @@ export { FlagLevelThree, Fullscreen, Folder, + Tag, Gallery, Gear, Globe, diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index f8c048ebc4c0..7f60ad3867c8 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -65,6 +65,7 @@ import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustra import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg'; import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg'; import SmallRocket from '@assets/images/simple-illustrations/simple-illustration__smallrocket.svg'; +import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg'; import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg'; import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; @@ -146,4 +147,5 @@ export { Workflows, ThreeLeggedLaptopWoman, House, + Tag, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 2a0139c64c07..2206b44899c2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1727,6 +1727,7 @@ export default { settings: 'Settings', reimburse: 'Reimbursements', categories: 'Categories', + tags: 'Tags', bills: 'Bills', invoices: 'Invoices', travel: 'Travel', @@ -1766,6 +1767,15 @@ export default { }, genericFailureMessage: 'An error occurred while updating the category, please try again.', }, + tags: { + requiresTag: 'Members must tag all spend', + enableTag: 'Enable tag', + subtitle: 'Tags add more detailed ways to classify costs.', + emptyTags: { + title: "You haven't created any tags", + subtitle: 'Add a tag to track projects, locations, departments, and more.', + }, + }, emptyWorkspace: { title: 'Create a workspace', subtitle: 'Workspaces are where you’ll chat with your team, reimburse expenses, issue cards, send invoices, pay bills, and more - all in one place.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 20f4cf8aeac8..9e63af9dc982 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1751,6 +1751,7 @@ export default { settings: 'Configuración', reimburse: 'Reembolsos', categories: 'Categorías', + tags: 'Etiquetas', bills: 'Pagar facturas', invoices: 'Enviar facturas', travel: 'Viajes', @@ -1790,6 +1791,15 @@ export default { }, genericFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.', }, + tags: { + requiresTag: 'Los miembros deben etiquetar todos los gastos', + enableTag: 'Habilitar etiqueta', + subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.', + emptyTags: { + title: 'No has creado ninguna etiqueta', + subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.', + }, + }, emptyWorkspace: { title: 'Crea un espacio de trabajo', subtitle: 'En los espacios de trabajo podrás chatear con tu equipo, reembolsar gastos, emitir tarjetas, enviar y pagar facturas, y mucho más - todo en un mismo lugar.', diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index 1e5d3639a32f..976699e31716 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -24,6 +24,7 @@ const workspaceSettingsScreens = { [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType, [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../../pages/workspace/categories/WorkspaceCategoriesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TAGS]: () => require('../../../../../pages/workspace/tags/WorkspaceTagsPage').default as React.ComponentType, } satisfies Screens; function BaseCentralPaneNavigator() { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 7e0e6c028ff1..8328e0e19688 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -67,6 +67,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES]: { path: ROUTES.WORKSPACE_CATEGORIES.route, }, + [SCREENS.WORKSPACE.TAGS]: { + path: ROUTES.WORKSPACE_TAGS.route, + }, }, }, [SCREENS.NOT_FOUND]: '*', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 6d680ac7e190..e1729f36e6f0 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -87,6 +87,9 @@ type CentralPaneNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES]: { policyID: string; }; + [SCREENS.WORKSPACE.TAGS]: { + policyID: string; + }; }; type WorkspaceSwitcherNavigatorParamList = { diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 571e4cafce74..a18d2667f1ea 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -169,6 +169,12 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.CATEGORIES, }, + { + translationKey: 'workspace.common.tags', + icon: Expensicons.Tag, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.TAGS, + }, ]; const menuItems: WorkspaceMenuItem[] = [ diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx new file mode 100644 index 000000000000..c82740eff361 --- /dev/null +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -0,0 +1,144 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo, useState} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import TableListItem from '@components/SelectionList/TableListItem'; +import Text from '@components/Text'; +import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; + +type PolicyForList = { + value: string; + text: string; + keyForList: string; + isSelected: boolean; + rightElement: React.ReactNode; +}; + +type WorkspaceTagsOnyxProps = { + /** Collection of tags attached to a policy */ + policyTags: OnyxEntry; +}; + +type WorkspaceTagsPageProps = WorkspaceTagsOnyxProps & StackScreenProps; + +function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { + const {isSmallScreenWidth} = useWindowDimensions(); + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + const [selectedTags, setSelectedTags] = useState>({}); + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + const tagList = useMemo( + () => + policyTagLists + .map((policyTagList) => + Object.values(policyTagList.tags).map((value) => ({ + value: value.name, + text: value.name, + keyForList: value.name, + isSelected: !!selectedTags[value.name], + rightElement: ( + + + {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')} + + + + + + ), + })), + ) + .flat(), + [policyTagLists, selectedTags, styles.alignSelfCenter, styles.disabledText, styles.flexRow, styles.p1, styles.pl2, theme.icon, translate], + ); + + const toggleTag = (tag: PolicyForList) => { + setSelectedTags((prev) => ({ + ...prev, + [tag.value]: !prev[tag.value], + })); + }; + + const toggleAllTags = () => { + const isAllSelected = tagList.every((tag) => !!selectedTags[tag.value]); + setSelectedTags(isAllSelected ? {} : Object.fromEntries(tagList.map((item) => [item.value, true]))); + }; + + const getCustomListHeader = () => ( + + {translate('common.name')} + {translate('statusPage.status')} + + ); + + return ( + + + + + + {translate('workspace.tags.subtitle')} + + {tagList.length ? ( + {}} + onSelectAll={toggleAllTags} + showScrollIndicator + ListItem={TableListItem} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + /> + ) : ( + + )} + + + + ); +} + +WorkspaceTagsPage.displayName = 'WorkspaceTagsPage'; + +export default withOnyx({ + policyTags: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`, + }, +})(WorkspaceTagsPage);