diff --git a/client/apps/project-portal-landingpage/package.json b/client/apps/project-portal-landingpage/package.json index 22908fadd..582bd91e8 100644 --- a/client/apps/project-portal-landingpage/package.json +++ b/client/apps/project-portal-landingpage/package.json @@ -1,6 +1,6 @@ { "name": "project-portal-landingpage", - "version": "0.0.2", + "version": "0.0.3", "description": "", "private": true, "type": "module", diff --git a/client/apps/project-portal-landingpage/src/components/PageHeader.tsx b/client/apps/project-portal-landingpage/src/components/PageHeader.tsx index 875efa721..6f5f08237 100644 --- a/client/apps/project-portal-landingpage/src/components/PageHeader.tsx +++ b/client/apps/project-portal-landingpage/src/components/PageHeader.tsx @@ -4,11 +4,12 @@ import { Typography } from '@equinor/eds-core-react'; import styled from 'styled-components'; import background from './assets/background.svg'; import { PropsWithChildren } from 'react'; +import { useCurrentUser } from '@equinor/fusion-framework-react/hooks'; export const StyledBackgroundWrapper = styled.section` background-image: url(${background}); width: 100%; - height: 100%; + height: calc(100vh - var(--header-height, 48px)); background-size: cover; background-repeat: no-repeat; background-position: bottom; @@ -46,14 +47,14 @@ export const getGreeting = () => { }; export const ProjectHeader = ({ children }: PropsWithChildren) => { - const { data } = { data: { name: 'chris' } }; //useCurrentUser(); + const user = useCurrentUser(); return ( Welcome to Project Portal - {getGreeting()} {data?.name || 'Chris'} + {getGreeting()} {user?.name} {children} diff --git a/client/apps/project-portal-landingpage/src/components/ProfileCardHeader.tsx b/client/apps/project-portal-landingpage/src/components/ProfileCardHeader.tsx new file mode 100644 index 000000000..7b5bfde01 --- /dev/null +++ b/client/apps/project-portal-landingpage/src/components/ProfileCardHeader.tsx @@ -0,0 +1,55 @@ +import styled from 'styled-components'; +import { Typography } from '@equinor/eds-core-react'; + +import { PersonAvatar } from '@equinor/fusion-react-person'; +import { PersonDetails } from '../types/person-details'; +import { Skeleton } from './skeleton/Skeleton'; +import { getDepartment, getJobTitle } from '../hooks/user'; + +const Style = { + InfoWrapper: styled.div<{ paddingNone?: boolean }>` + display: flex; + align-items: center; + gap: 1rem; + padding: ${({ paddingNone }) => (paddingNone ? '0px' : '1rem')}; + `, +}; + +export const ProfileCardHeader = ({ + user, + trigger = 'none', + paddingNone, +}: { + user?: PersonDetails; + trigger?: 'click' | 'hover' | 'none'; + paddingNone?: boolean; +}) => { + if (!user) { + return ( + + + +
+ +
+ + +
+
+
+ ); + } + + return ( + + +
+ {user?.name} +
+ {getDepartment(user)} + {getJobTitle(user)} +
+
+
+ ); +}; diff --git a/client/apps/project-portal-landingpage/src/components/ProjectPortalPage.tsx b/client/apps/project-portal-landingpage/src/components/ProjectPortalPage.tsx index 0b38504c1..2840192e5 100644 --- a/client/apps/project-portal-landingpage/src/components/ProjectPortalPage.tsx +++ b/client/apps/project-portal-landingpage/src/components/ProjectPortalPage.tsx @@ -12,7 +12,8 @@ import { useFeature } from '@equinor/fusion-framework-react-app/feature-flag'; import { useUserOrgDetails } from '../hooks/user'; import InfoBox from './InfoBox/InfoBox'; -import { useNavigateOnContextChange } from '../hooks/use-navigate-on-context-change'; +// import { useNavigateOnContextChange } from '../hooks/use-navigate-on-context-change'; +import { User } from './user/UserCard'; // const styles = { // contentSection: css` @@ -129,7 +130,7 @@ export const ProjectPortalPage = (): JSX.Element => { - {/* */} + diff --git a/client/apps/project-portal-landingpage/src/components/skeleton/Skeleton.tsx b/client/apps/project-portal-landingpage/src/components/skeleton/Skeleton.tsx new file mode 100644 index 000000000..75eb593c4 --- /dev/null +++ b/client/apps/project-portal-landingpage/src/components/skeleton/Skeleton.tsx @@ -0,0 +1,58 @@ +import FusionSkeleton, { SkeletonSize, SkeletonVariant } from '@equinor/fusion-react-skeleton'; +import { CSSProperties, FC } from 'react'; + +type SkeletonProps = { + width?: number | string; + height?: number | string; + size?: keyof typeof skeletonSize; + variant?: keyof typeof skeletonVariant; + fluid?: boolean; +}; + +const skeletonVariant = { + circle: SkeletonVariant.Circle, + rectangle: SkeletonVariant.Rectangle, + square: SkeletonVariant.Square, + text: SkeletonVariant.Text, +}; + +const skeletonSize = { + xSmall: SkeletonSize.XSmall, + small: SkeletonSize.small, + large: SkeletonSize.Large, + medium: SkeletonSize.Medium, +}; + +/** + * Skeleton Component + * + * The `Skeleton` component is a simplified ree-export of `@equinor/fusion-react-skeleton` a React component used to render skeleton loading elements. + * + * @param width - number (optional) - Specifies the width of the skeleton element in present% + * @param type - string (optional) - Specifies the type of skeleton to render. Should be one of "xSmall" | "small" | "large" | "medium" default is xSmall. + * @param variant - string (optional) - Specifies the variant or shape of the skeleton. Should be one of "circle" | "rectangle" | "square" | "text", default is text. + * @param fluid - boolean (optional) - Expands the skeleton element width to the width of the parent + * + * @returns JSX.Element - A skeleton loading element with the specified type, variant, and width (if provided). + */ +export const Skeleton: FC = ({ + width, + height, + size, + variant, + style, + fluid, +}) => { + return ( + + ); +}; diff --git a/client/apps/project-portal-landingpage/src/components/user/UserCard.tsx b/client/apps/project-portal-landingpage/src/components/user/UserCard.tsx new file mode 100644 index 000000000..b092a1b43 --- /dev/null +++ b/client/apps/project-portal-landingpage/src/components/user/UserCard.tsx @@ -0,0 +1,152 @@ +import { Card, Typography, Icon } from '@equinor/eds-core-react'; + +import { tokens } from '@equinor/eds-tokens'; +import styled from 'styled-components'; + +import { external_link, tag_relations } from '@equinor/eds-icons'; + +import { useMemo } from 'react'; +import { useCurrentUser } from '../../hooks/user'; +import { useRelationsByType } from '../../context'; +import { PersonPosition } from '../../types/person-details'; +import { ProfileCardHeader } from '../ProfileCardHeader'; +import { useFrameworkCurrentContext } from '@equinor/fusion-framework-react-app/context'; + +declare global { + interface Window { + _config_: { + fusionLegacyEnvIdentifier: string; + }; + } +} + +export const getFusionPortalURL = () => { + switch (window._config_.fusionLegacyEnvIdentifier.toLowerCase()) { + case 'fprd': + return 'https://fusion.equinor.com'; + case 'ci': + return 'https://fusion-s-portal-ci.azurewebsites.net'; + case 'fqa': + return 'https://fusion-s-portal-fqa.azurewebsites.net'; + default: + return 'https://fusion-s-portal-ci.azurewebsites.net'; + } +}; + +const Style = { + Wrapper: styled.div` + display: flex; + flex-direction: column; + gap: ${tokens.spacings.comfortable.medium}; + padding: ${tokens.spacings.comfortable.medium}; + `, + PositionWrapper: styled.span` + display: flex; + flex-direction: column; + `, + ProjectHeader: styled.div` + display: flex; + justify-content: space-between; + border-bottom: 1px solid ${tokens.colors.ui.background__medium.hex}; + padding: ${tokens.spacings.comfortable.small}; + `, + PositionLink: styled.a` + height: auto; + border: 1px solid ${tokens.colors.ui.background__medium.hex}; + color: ${tokens.colors.interactive.primary__resting.hex}; + border-radius: 4px; + width: 100%; + text-decoration: none; + :hover { + background-color: ${tokens.colors.interactive.primary__hover_alt.hex}; + } + :focus { + background-color: ${tokens.colors.interactive.primary__hover_alt.hex}; + } + `, + + Content: styled.div` + padding: ${tokens.spacings.comfortable.medium_small}; + display: flex; + align-items: center; + height: auto; + `, + Icon: styled(Icon)` + padding-right: 1rem; + `, +}; + +export const ProjectPosition = ({ positions }: { positions?: PersonPosition[] }) => { + const { currentContext } = useFrameworkCurrentContext(); + const { data: equinorTask } = useRelationsByType('OrgChart', currentContext?.id); + + const projectPositions = useMemo(() => { + return ( + positions?.filter((item) => { + return ( + item.appliesTo && + new Date(item.appliesTo) > new Date() && + item.project.id === equinorTask[0]?.externalId + ); + }) || [] + ); + }, [positions, equinorTask]); + + return ( + <> + { + projectPositions.length > 0 ? ( + + {projectPositions.map((position) => ( + + + + {position.project.name} + + + + +
+ + {position.name} + + + <> + {position.appliesFrom && + new Date(position.appliesFrom).toLocaleDateString('en-US')} + {' - '} + {position.appliesTo && + new Date(position.appliesTo).toLocaleDateString('en-US')} + ({position.workload}%) + + +
+
+
+
+ ))} +
+ ) : null // + } + + ); +}; + +export const User = () => { + const user = useCurrentUser(); + + return ( + + + + + ); +}; diff --git a/client/apps/project-portal-landingpage/src/context/hooks/use-relations-by-type.ts b/client/apps/project-portal-landingpage/src/context/hooks/use-relations-by-type.ts new file mode 100644 index 000000000..c0deb5ab1 --- /dev/null +++ b/client/apps/project-portal-landingpage/src/context/hooks/use-relations-by-type.ts @@ -0,0 +1,21 @@ +import { useEffect, useMemo, useState } from 'react'; +import { RelationsTypes } from '../types/relations'; +import { useContextRelationsQuery } from '../queries/get-context-relations'; + +export function useRelationsByType(type: RT, contextId?: string) { + const [error, setError] = useState(); + const { data, isLoading } = useContextRelationsQuery(contextId); + + const filteredRelations = useMemo(() => { + setError(undefined); + return data?.filter((relation) => relation.type.id === type) || []; + }, [data]); + + useEffect(() => { + if (!isLoading && filteredRelations?.length === 0) { + setError(Error(`No context relation found for ${type}`)); + } + }, [isLoading, filteredRelations]); + + return { data: filteredRelations, isLoading, error }; +} diff --git a/client/apps/project-portal-landingpage/src/context/index.ts b/client/apps/project-portal-landingpage/src/context/index.ts new file mode 100644 index 000000000..9fe0e097e --- /dev/null +++ b/client/apps/project-portal-landingpage/src/context/index.ts @@ -0,0 +1,2 @@ +export * from './types/relations'; +export { useRelationsByType } from './hooks/use-relations-by-type'; diff --git a/client/apps/project-portal-landingpage/src/context/queries/get-context-relations.ts b/client/apps/project-portal-landingpage/src/context/queries/get-context-relations.ts new file mode 100644 index 000000000..2a00d7867 --- /dev/null +++ b/client/apps/project-portal-landingpage/src/context/queries/get-context-relations.ts @@ -0,0 +1,25 @@ +import { useFramework } from '@equinor/fusion-framework-react'; +import { useQuery } from '@tanstack/react-query'; +import { IHttpClient } from '@equinor/fusion-framework-module-http'; +import { RelationReturnType, RelationsTypes } from '../types/relations'; + +export async function getContextRelations( + client: IHttpClient, + contextId?: string, + signal?: AbortSignal +): Promise[] | undefined> { + if (!contextId) return; + const res = await client.fetch(`/contexts/${contextId}/relations`, { signal }); + if (!res.ok) throw res; + return (await res.json()) as RelationReturnType[]; +} + +export const useContextRelationsQuery = (contextId?: string) => { + const client = useFramework().modules.serviceDiscovery.createClient('context'); + + return useQuery({ + queryKey: ['context-relations', contextId], + queryFn: async ({ signal }) => getContextRelations(await client, contextId, signal), + enabled: Boolean(contextId), + }); +}; diff --git a/client/apps/project-portal-landingpage/src/context/types/relations.ts b/client/apps/project-portal-landingpage/src/context/types/relations.ts new file mode 100644 index 000000000..9bb204243 --- /dev/null +++ b/client/apps/project-portal-landingpage/src/context/types/relations.ts @@ -0,0 +1,134 @@ + + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export type RelationsTypes = + | 'EquinorTask' + | 'Contract' + | 'ProjectMaster' + | 'PimsDomain' + | 'Project' + | 'OrgChart' + | 'Facility'; + +export type RelationReturnType = T extends 'ProjectMaster' + ? Relations + : T extends 'Facility' + ? Relations + : T extends 'OrgChart' + ? Relations + : T extends 'EquinorTask' + ? Relations + : T extends 'Contract' + ? Relations + : T extends 'Project' + ? Relations + : T extends 'PimsDomain' + ? Relations + : Relations; + +export interface Relations { + relationSource: string; + relationType?: any; + id: string; + externalId: string; + source?: any; + type: Type; + value: T extends object ? T : Value; + title: string; + isActive: boolean; + isDeleted: boolean; + created: string; + updated?: string; +} + +export type ProjectMaster = { + facilities: string[]; + projectCategory: string; + cvpid: string; + documentManagementId: string; + phase: string; + portfolioOrganizationalUnit: string; +} & Record; + +interface Value { + identity?: string; + sapPlant?: string; + schema?: string; + subFacilities?: string[]; + parentFacility?: any; + contractNumber?: string; + companyName?: string; + startDate?: string; + endDate?: string; + projectMasterId?: string; + isValid?: boolean; + taskName?: string; + taskType?: string; + taskState?: string; + orgChartId?: string; + orgUnitSapId?: string; + orgUnitShortName?: string; + orgUnitName?: string; + orgUnitDepartment?: string; + orgUnitFullDepartment?: string; + orgUnitType?: string; + domainId?: string; + dgPhase?: string; + siteCode?: string; + projectIdentifier?: string; + wbs?: string; +} + +export interface Contract { + contractNumber?: number; + companyName?: string; + startDate?: string; + endDate?: string; + projectMasterId?: string; + isValid: boolean; +} + +export interface Facility { + identity: string; + sapPlant: string; + schema: string; + subFacilities?: string[]; + parentFacility?: string[]; +} + +export interface OrgChart { + orgChartId?: string; + domainId?: string; + dgPhase?: string; +} + +interface EquinorTask { + taskName?: string; + taskType?: string; + taskState?: string; + orgChartId?: string; + orgUnitSapId?: string; + orgUnitShortName?: string; + orgUnitName?: string; + orgUnitDepartment?: string; + orgUnitFullDepartment?: string; + orgUnitType?: string; + projectMasterId?: string; +} + +interface PDP { + siteCode: string; + projectIdentifier: string; +} + +interface PimsDomain { + domainId?: string; + projectMasterId?: string; +} + +interface Type { + id: string; + isChildType: boolean; + parentTypeIds: string[]; +} diff --git a/client/packages/portal-pages/src/pages/project-portal-page/user/UserCard.tsx b/client/packages/portal-pages/src/pages/project-portal-page/user/UserCard.tsx index 11ff4f619..81b654436 100644 --- a/client/packages/portal-pages/src/pages/project-portal-page/user/UserCard.tsx +++ b/client/packages/portal-pages/src/pages/project-portal-page/user/UserCard.tsx @@ -1,5 +1,3 @@ -import { useCurrentUser, useRelationsByType } from '@portal/core'; - import { Card, Typography, Icon } from '@equinor/eds-core-react'; import { ProfileCardHeader } from '@portal/components';