Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add employee profile page #12371

Merged
merged 10 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/web/src/components/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ const createRoute = (locale: Locales) =>
"../pages/Notifications/NotificationsPage/NotificationsPage"
),
},
{
path: "employee-profile",
lazy: () =>
import("../pages/EmployeeProfile/EmployeeProfilePage"),
},
{
path: "personal-information",
lazy: () =>
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/hooks/useRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ const getRoutes = (lang: Locales) => {
);
},

// Employee profile
employeeProfile: () => `${applicantUrl}/employee-profile`,

skillPortfolio: () => [applicantUrl, "skills"].join("/"),
skillShowcase: () => [showcase].join("/"),
editUserSkill: (skillId: string) =>
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@
"defaultMessage": "SΓ©lectionnez la capacitΓ© linguistique de travail dont le candidat a besoin pour ce poste. La capacitΓ© linguistique sΓ©lectionnΓ©e sera comparΓ©e Γ  celle choisie par les candidats dans leur demande. Γ€ noter, les candidats qui ont sΓ©lectionnΓ© Bilingue peuvent ne pas avoir des rΓ©sultats d’évaluation de langue seconde du gouvernement du Canada.",
"description": "Message describing the work language ability filter in the search form."
},
"+RDFZH": {
"defaultMessage": "GΓ©rez vos renseignements d’employΓ©e ou d’employΓ© du gouvernement, y compris vos prΓ©fΓ©rences en matiΓ¨re de dΓ©veloppement de carriΓ¨re et les faΓ§ons de travailler.",
"description": "Description of the employee profile page"
},
"+SoiCw": {
"defaultMessage": "Endroits exclus",
"description": "Location specifics label"
Expand Down Expand Up @@ -1631,6 +1635,10 @@
"defaultMessage": "Vos options en matière d'équité",
"description": "Title for the subsection for viewing/selecting employment equity"
},
"6KS1jD": {
"defaultMessage": "Nous aimerions en savoir plus sur le cheminement de carrière que vous souhaitez suivre. Fournir des renseignements sur vos préférences et vos aspirations aidera les gestionnaires de talents à prendre des décisions plus éclairées dans le cadre d'une promotion, d'une mutation latérale ou d'une occasion de perfectionnement professionnel.",
"description": "Lead-in text explaining the users career plan"
},
"6Q1N7Z": {
"defaultMessage": "Veuillez sΓ©lectionner les expΓ©riences dansβ€―:",
"description": "Message before skills list in application education page."
Expand Down Expand Up @@ -8518,6 +8526,10 @@
"defaultMessage": "Le Programme s’adresse aux PremiΓ¨res Nations, aux Inuits et aux MΓ©tis. Si vous Γͺtes un membre des PremiΓ¨res Nations, un Inuit ou un MΓ©tis et que vous avez une passion pour la technologie, ce programme est pour vous!",
"description": "First paragraph about who the program is for"
},
"f1reMg": {
"defaultMessage": "Poste de rΓͺve",
"description": "Title for a users dream role information"
},
"f3KZGn": {
"defaultMessage": "Les cercles de partage, l’accΓ¨s Γ  unΒ·e conseillerΒ·Γ¨re en soutien Γ  la formation en TI et les facilitateursΒ·trices de rΓ©ussite pour les apprentiΒ·es sont autant de soutiens supplΓ©mentaires. Si les facilitateursΒ·trices sont avant tout une ressource pour les apprentiΒ·es, ils, elles constituent Γ©galement un systΓ¨me de soutien pour les gestionnaires et les superviseurΒ·es.",
"description": "Paragraph 2 of the 'Circle of Support' subsection"
Expand Down Expand Up @@ -10542,6 +10554,10 @@
"defaultMessage": "Si vous avez dΓ©jΓ  un compte de ClΓ©GC, vous pouvez ouvrir une session dans votre profil sur la plateforme Talents numΓ©riques du GC Γ  l'aide de votre ClΓ©GC, mΓͺme si vous n'avez jamais utilisΓ© cette plateforme avant. Si vous ne savez pas si vous avez dΓ©jΓ  un compte de ClΓ©GC, continuez vers le site Web et essayez d'ouvrir une session. Si vous ne vous souvenez pas de votre mot de passe, c'est lΓ  que vous pourrez aussi le rΓ©initialiser.",
"description": "GCKey answer for when user already has an account"
},
"okKbgd": {
"defaultMessage": "Préférences en matière de développement de carrière",
"description": "Title for a users career development preferences"
},
"okRYhl": {
"defaultMessage": "Ce que nous entendons",
"description": "Title of a quotes section"
Expand Down Expand Up @@ -10922,6 +10938,10 @@
"defaultMessage": "Processus dupliqué avec succès!",
"description": "Message displayed to user after pool is duplicated"
},
"qZKabV": {
"defaultMessage": "Objectifs et style de travail",
"description": "Title for a users goals and work style"
},
"qek0N+": {
"defaultMessage": "Bienvenue dans votre panneau de notification. Cliquez ou activez une notification pour accΓ©der Γ  la page concernΓ©e. Chaque notification peut Γͺtre marquΓ©e comme lue ou supprimΓ©e.",
"description": "Instructions on how to manage notifications"
Expand Down Expand Up @@ -11470,6 +11490,10 @@
"defaultMessage": "50 000 $ Γ  1 million de dollars",
"description": "Contract value range between fifty-thousand and one-million"
},
"u+lXsz": {
"defaultMessage": "Votre profil d'employΓ©e ou d'employΓ©",
"description": "Page title for a users employee profile"
},
"u3CPra": {
"defaultMessage": "Recevoir une notification lorsqu'un nouvel emploi est publiΓ© sur la plateforme.",
"description": "Subtitle for job alerts checklist"
Expand Down Expand Up @@ -12414,6 +12438,10 @@
"defaultMessage": "Veuillez ne pas fournir des renseignements personnels supplΓ©mentaires qui ne sont pas requis Γ  cette fin.",
"description": "Paragraph for privacy policy page"
},
"zN7MBv": {
"defaultMessage": "Planification de carrière",
"description": "Title for a users career plan"
},
"zNgDex": {
"defaultMessage": "Zone de sΓ©lection",
"description": "Label for a process' area of selection"
Expand Down
219 changes: 219 additions & 0 deletions apps/web/src/pages/EmployeeProfile/EmployeeProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { useIntl } from "react-intl";
import { useQuery } from "urql";
import ChartBarSquareIcon from "@heroicons/react/24/outline/ChartBarSquareIcon";

import { navigationMessages } from "@gc-digital-talent/i18n";
import { FragmentType, getFragment, graphql } from "@gc-digital-talent/graphql";
import {
Heading,
Pending,
TableOfContents,
ThrowNotFound,
} from "@gc-digital-talent/ui";
import { ROLE_NAME } from "@gc-digital-talent/auth";
import { UnauthorizedError } from "@gc-digital-talent/helpers";

import Hero from "~/components/Hero";
import SEO from "~/components/SEO/SEO";
import useBreadcrumbs from "~/hooks/useBreadcrumbs";
import useRoutes from "~/hooks/useRoutes";
import RequireAuth from "~/components/RequireAuth/RequireAuth";
import { isVerifiedGovEmployee } from "~/utils/userUtils";
import profileMessages from "~/messages/profileMessages";
import StatusItem from "~/components/StatusItem/StatusItem";

import messages from "./messages";

const SECTION_ID = {
CAREER_PLANNING: "career-planning-section",
CAREER_DEVELOPMENT: "career-development-section",
DREAM_ROLE: "dream-role-section",
GOALS_WORK_STYLE: "goals-work-style-section",
};

const EmployeeProfile_Fragment = graphql(/** GraphQL */ `
fragment EmployeeProfile on User {
isGovEmployee
workEmail
isWorkEmailVerified
}
`);

interface EmployeeProfileProps {
userQuery: FragmentType<typeof EmployeeProfile_Fragment>;
}

const EmployeeProfile = ({ userQuery }: EmployeeProfileProps) => {
const intl = useIntl();
const paths = useRoutes();
const user = getFragment(EmployeeProfile_Fragment, userQuery);
const { isGovEmployee, workEmail, isWorkEmailVerified } = user;

if (
!isVerifiedGovEmployee({ isGovEmployee, workEmail, isWorkEmailVerified })
) {
throw new UnauthorizedError(
intl.formatMessage({
defaultMessage: "Not a verified employee",
id: "Ljv0T9",
description: "Error message for unauthorized employee access",
}),
);
}

const pageTitle = intl.formatMessage({
defaultMessage: "Your employee profile",
id: "u+lXsz",
description: "Page title for a users employee profile",
});

const subtitle = intl.formatMessage({
defaultMessage:
"Manage your government employee information, including career development preferences and work styles.",
id: "+RDFZH",
description: "Description of the employee profile page",
});

const crumbs = useBreadcrumbs({
crumbs: [
{
label: intl.formatMessage(navigationMessages.profileAndApplications),
petertgiles marked this conversation as resolved.
Show resolved Hide resolved
url: paths.profileAndApplications(),
},
{
url: paths.employeeProfile(),
label: intl.formatMessage({
defaultMessage: "Employee profile",
id: "FThj7q",
description: "Short title for a users employee profile",
}),
},
],
});

return (
<>
<SEO title={pageTitle} description={subtitle} />
<Hero title={pageTitle} subtitle={subtitle} crumbs={crumbs} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In mobile widths, the Hero should be centred. The section headers should be, too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is just using our base hero/heading components. That would require modifying it specifically for this page which seems odd. I feel like we should either:

  1. Update the base component so we have consistent styles on all pages
  2. Or leave this page alone so we don't have special styles for one page

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote: leave this page alone so we don't have special styles for one page.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, the hero does go centred on its own. I didn't test narrow enough.
image

All the admin page refreshes use centred headings in mobile widths. I don't know if this is standard enough to include in the base component or not.
image

<div data-h2-wrapper="base(center, large, x1) p-tablet(center, large, x2)">
<TableOfContents.Wrapper data-h2-padding-top="base(x3)">
<TableOfContents.Navigation>
<TableOfContents.List
data-h2-padding-left="base(x.5)"
data-h2-list-style-type="base(none)"
>
<TableOfContents.ListItem>
<StatusItem
asListItem={false}
title={intl.formatMessage(messages.careerPlanning)}
status="success"
scrollTo={SECTION_ID.CAREER_PLANNING}
/>
<TableOfContents.List
data-h2-padding-left="base(x.5)"
data-h2-list-style-type="base(none)"
>
<TableOfContents.ListItem>
<StatusItem
asListItem={false}
title={intl.formatMessage(messages.careerDevelopment)}
status="success"
scrollTo={SECTION_ID.CAREER_DEVELOPMENT}
/>
</TableOfContents.ListItem>
<TableOfContents.ListItem>
<StatusItem
asListItem={false}
title={intl.formatMessage(messages.dreamRole)}
status="success"
scrollTo={SECTION_ID.DREAM_ROLE}
/>
</TableOfContents.ListItem>
<TableOfContents.ListItem>
<StatusItem
asListItem={false}
title={intl.formatMessage(messages.goalsWorkStyle)}
status="success"
scrollTo={SECTION_ID.GOALS_WORK_STYLE}
/>
</TableOfContents.ListItem>
</TableOfContents.List>
</TableOfContents.ListItem>
</TableOfContents.List>
</TableOfContents.Navigation>
<TableOfContents.Content>
<div
data-h2-display="base(flex)"
data-h2-flex-direction="base(column)"
data-h2-gap="base(x3 0)"
>
<TableOfContents.Section id={SECTION_ID.CAREER_PLANNING}>
<Heading
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Figma, this should be font-weight 400.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I can do that but this is also using the base heading component and we should avoid changing font styles for headings because they need to be consistent for visual users to indicate content hierarchy for accessibility.

Should the heading 2 have 400? If not, I don;'t think we should be messing with font weight since it can gave a negative affect on the sites accessibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the designers have explicitly said there is no standard font weight for a given heading level and should be set on a case-by-case basis.
#11859 (comment)
You could check in with a designer and see if the design could be changed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For both heading related things:

  • h1, h2, h3 at a minimum should always be centered on mobile devices
  • h2s will generally be 400 weight in most cases if you need to set a default on the component - this has been the case for a while in the designs and it's fine to propagate it everywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are editing the base component, would it be better to create a separate ticket to fix our base heading? It seems a little out of scope for a basic stub page to affect all other pages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll write one up. πŸ™ƒ
#11859

level="h2"
Icon={ChartBarSquareIcon}
color="secondary"
data-h2-margin-top="base(0)"
>
{intl.formatMessage(messages.careerPlanning)}
</Heading>
<p>
{intl.formatMessage({
defaultMessage:
"We'd like to learn more about the career path you'd like to follow. Providing information about preferences and aspirations will help talent managers make more informed decisions when you've been nominated for a promotion, lateral movement, or professional development opportunity.",
petertgiles marked this conversation as resolved.
Show resolved Hide resolved
id: "6KS1jD",
description:
"Lead-in text explaining the users career plan",
})}
</p>
</TableOfContents.Section>
<TableOfContents.Section
id={SECTION_ID.CAREER_DEVELOPMENT}
></TableOfContents.Section>
<TableOfContents.Section
id={SECTION_ID.DREAM_ROLE}
></TableOfContents.Section>
<TableOfContents.Section
id={SECTION_ID.GOALS_WORK_STYLE}
></TableOfContents.Section>
</div>
</TableOfContents.Content>
</TableOfContents.Wrapper>
</div>
</>
);
};

const EmployeeProfilePage_Query = graphql(/** GraphQL */ `
query EmployeeProfilePage {
me {
...EmployeeProfile
}
}
`);

const EmployeeProfilePage = () => {
const intl = useIntl();
const [{ data, fetching, error }] = useQuery({
query: EmployeeProfilePage_Query,
});

return (
<Pending fetching={fetching} error={error}>
{data?.me ? (
<EmployeeProfile userQuery={data.me} />
) : (
<ThrowNotFound
message={intl.formatMessage(profileMessages.userNotFound)}
/>
)}
</Pending>
);
};

export const Component = () => (
<RequireAuth roles={[ROLE_NAME.Applicant]}>
<EmployeeProfilePage />
</RequireAuth>
);

Component.displayName = "EmployeeProfilePage";
24 changes: 24 additions & 0 deletions apps/web/src/pages/EmployeeProfile/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineMessages } from "react-intl";

export default defineMessages({
careerPlanning: {
defaultMessage: "Career planning",
id: "zN7MBv",
description: "Title for a users career plan",
},
careerDevelopment: {
defaultMessage: "Career development preferences",
id: "okKbgd",
description: "Title for a users career development preferences",
},
dreamRole: {
defaultMessage: "Dream role",
id: "f1reMg",
description: "Title for a users dream role information",
},
goalsWorkStyle: {
defaultMessage: "Goals and work style",
id: "qZKabV",
description: "Title for a users goals and work style",
},
});
14 changes: 14 additions & 0 deletions apps/web/src/utils/userUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,17 @@ export const formatLocation = ({

return intl.formatMessage(commonMessages.notProvided);
};

interface IsVerifiedGovEmployeeArgs {
isGovEmployee?: Maybe<boolean>;
workEmail?: Maybe<string>;
isWorkEmailVerified?: Maybe<boolean>;
}

export const isVerifiedGovEmployee = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@esizer Does this function satisfy the acceptance criteria for #12363 and then allow it to be closed as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May need to wait and see. I think to but not 100% sure

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this does satisfy 12363.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isWorkEmailVerified is computed server-side making it not 100% frontend but I'm not sure if that is a problem. Issue linked has no A/C or details.

isGovEmployee,
workEmail,
isWorkEmailVerified,
}: IsVerifiedGovEmployeeArgs): boolean => {
return Boolean(isGovEmployee && workEmail && isWorkEmailVerified);
petertgiles marked this conversation as resolved.
Show resolved Hide resolved
};
Loading