diff --git a/backend/lcfs/db/migrations/versions/2025-01-06-22-09_fa98709e7952.py b/backend/lcfs/db/migrations/versions/2025-01-06-22-09_fa98709e7952.py
new file mode 100644
index 000000000..2b815de67
--- /dev/null
+++ b/backend/lcfs/db/migrations/versions/2025-01-06-22-09_fa98709e7952.py
@@ -0,0 +1,177 @@
+"""Add legacy fuel types
+
+Revision ID: fa98709e7952
+Revises: 94306eca5261
+Create Date: 2025-01-06 22:09:52.936619
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "fa98709e7952"
+down_revision = "94306eca5261"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.execute(
+ """
+ INSERT INTO fuel_type (
+ fuel_type,
+ fossil_derived,
+ other_uses_fossil_derived,
+ default_carbon_intensity,
+ units,
+ unrecognized,
+ create_user,
+ update_user,
+ is_legacy
+ )
+ VALUES
+ -- 1) Natural gas-based gasoline
+ (
+ 'Natural gas-based gasoline',
+ FALSE,
+ TRUE,
+ 90.07,
+ 'Litres',
+ FALSE,
+ 'no_user',
+ 'no_user',
+ TRUE
+ ),
+ -- 2) Petroleum-based diesel
+ (
+ 'Petroleum-based diesel',
+ FALSE,
+ TRUE,
+ 94.76,
+ 'Litres',
+ FALSE,
+ 'no_user',
+ 'no_user',
+ TRUE
+ ),
+ -- 3) Petroleum-based gasoline
+ (
+ 'Petroleum-based gasoline',
+ FALSE,
+ TRUE,
+ 88.14,
+ 'Litres',
+ FALSE,
+ 'no_user',
+ 'no_user',
+ TRUE
+ );
+ """
+ )
+
+ op.execute(
+ """
+ INSERT INTO energy_density (
+ fuel_type_id,
+ density,
+ uom_id,
+ create_user,
+ update_user
+ )
+ SELECT
+ ft.fuel_type_id,
+ CASE
+ WHEN ft.fuel_type = 'Natural gas-based gasoline' THEN 34.69
+ WHEN ft.fuel_type = 'Petroleum-based diesel' THEN 38.65
+ WHEN ft.fuel_type = 'Petroleum-based gasoline' THEN 34.69
+ END AS density,
+ 1 AS uom_id,
+ 'no_user' AS create_user,
+ 'no_user' AS update_user
+ FROM fuel_type ft
+ WHERE ft.fuel_type IN (
+ 'Natural gas-based gasoline',
+ 'Petroleum-based diesel',
+ 'Petroleum-based gasoline'
+ );
+ """
+ )
+
+ op.execute(
+ """
+ INSERT INTO energy_effectiveness_ratio (
+ fuel_category_id,
+ fuel_type_id,
+ end_use_type_id,
+ ratio,
+ create_user,
+ update_user,
+ effective_date,
+ effective_status,
+ expiration_date
+ )
+ SELECT
+ CASE
+ WHEN ft.fuel_type = 'Petroleum-based diesel' THEN 2
+ ELSE 1
+ END AS fuel_category_id,
+ ft.fuel_type_id,
+ NULL AS end_use_type_id,
+ 1.0 AS ratio,
+ 'no_user' AS create_user,
+ 'no_user' AS update_user,
+ CURRENT_DATE AS effective_date,
+ TRUE AS effective_status,
+ NULL AS expiration_date
+ FROM fuel_type ft
+ WHERE ft.fuel_type IN (
+ 'Natural gas-based gasoline',
+ 'Petroleum-based diesel',
+ 'Petroleum-based gasoline'
+ );
+ """
+ )
+
+
+def downgrade():
+ op.execute(
+ """
+ DELETE FROM energy_effectiveness_ratio
+ WHERE fuel_type_id IN (
+ SELECT fuel_type_id
+ FROM fuel_type
+ WHERE fuel_type IN (
+ 'Natural gas-based gasoline',
+ 'Petroleum-based diesel',
+ 'Petroleum-based gasoline'
+ )
+ );
+ """
+ )
+
+ op.execute(
+ """
+ DELETE FROM energy_density
+ WHERE fuel_type_id IN (
+ SELECT fuel_type_id
+ FROM fuel_type
+ WHERE fuel_type IN (
+ 'Natural gas-based gasoline',
+ 'Petroleum-based diesel',
+ 'Petroleum-based gasoline'
+ )
+ );
+ """
+ )
+
+ op.execute(
+ """
+ DELETE FROM fuel_type
+ WHERE fuel_type IN (
+ 'Natural gas-based gasoline',
+ 'Petroleum-based diesel',
+ 'Petroleum-based gasoline'
+ );
+ """
+ )
diff --git a/frontend/src/assets/locales/en/dashboard.json b/frontend/src/assets/locales/en/dashboard.json
index c88fa945b..7ec7521dc 100644
--- a/frontend/src/assets/locales/en/dashboard.json
+++ b/frontend/src/assets/locales/en/dashboard.json
@@ -65,5 +65,17 @@
"inProgress": "Compliance report(s) in progress",
"awaitingGovReview": "Compliance report(s) awaiting government review",
"noActionRequired": "There are no reports that require any action."
+ },
+ "userSettings": {
+ "title": "User settings",
+ "notifications": "Notifications",
+ "configureNotifications": "Configure your notifications",
+ "help": "Help"
+ },
+ "orgUserSettings": {
+ "title": "User settings",
+ "notifications": "Notifications",
+ "configureNotifications": "Configure your notifications",
+ "help": "Help"
}
}
diff --git a/frontend/src/views/Dashboard/Dashboard.jsx b/frontend/src/views/Dashboard/Dashboard.jsx
index 7d6a66052..2708d076e 100644
--- a/frontend/src/views/Dashboard/Dashboard.jsx
+++ b/frontend/src/views/Dashboard/Dashboard.jsx
@@ -3,15 +3,20 @@ import { Grid, Box } from '@mui/material'
import { Role } from '@/components/Role'
import { roles, govRoles, nonGovRoles } from '@/constants/roles'
import {
+ // IDIR Cards
AdminLinksCard,
+ DirectorReviewCard,
+ TransactionsCard,
+ UserSettingsCard,
+
+ // BCeID Cards
OrgDetailsCard,
OrgBalanceCard,
FeedbackCard,
WebsiteCard,
- DirectorReviewCard,
- TransactionsCard,
OrgTransactionsCard,
- OrgComplianceReportsCard
+ OrgComplianceReportsCard,
+ OrgUserSettingsCard
} from './components/cards'
import OrganizationsSummaryCard from './components/cards/idir/OrganizationsSummaryCard'
@@ -93,6 +98,14 @@ export const Dashboard = () => {
+
+ {/* User settings */}
+
+
+
+
+
+
diff --git a/frontend/src/views/Dashboard/components/cards/bceid/OrgUserSettingsCard.jsx b/frontend/src/views/Dashboard/components/cards/bceid/OrgUserSettingsCard.jsx
new file mode 100644
index 000000000..18a45546d
--- /dev/null
+++ b/frontend/src/views/Dashboard/components/cards/bceid/OrgUserSettingsCard.jsx
@@ -0,0 +1,79 @@
+import React from 'react'
+import { List, ListItemButton, Stack, Typography } from '@mui/material'
+import { useNavigate } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
+import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard'
+import BCTypography from '@/components/BCTypography'
+import withRole from '@/utils/withRole'
+import { nonGovRoles } from '@/constants/roles'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
+import { useCurrentUser } from '@/hooks/useCurrentUser'
+import { ROUTES } from '@/constants/routes'
+
+const linkStyle = {
+ textDecoration: 'underline',
+ color: 'link.main',
+ '&:hover': { color: 'info.main' }
+}
+
+const UserSettingsLink = ({ onClick, children }) => (
+
+
+ {children}
+
+
+)
+
+const OrgUserSettingsCard = () => {
+ const { t } = useTranslation(['dashboard'])
+ const { data: currentUser } = useCurrentUser()
+ const navigate = useNavigate()
+
+ const { title, firstName, lastName } = currentUser || {}
+ const name = [firstName, lastName].filter(Boolean).join(' ')
+ const displayName = [name, title].filter(Boolean).join(', ')
+
+ return (
+
+
+ {displayName}
+
+
+
+ navigate(ROUTES.NOTIFICATIONS)}>
+ {t('dashboard:orgUserSettings.notifications')}
+
+
+ navigate(ROUTES.NOTIFICATIONS_SETTINGS)}
+ >
+ {t('dashboard:orgUserSettings.configureNotifications')}
+
+
+ {/* TODO: Update the link to the help page */}
+ navigate()}>
+ {t('dashboard:orgUserSettings.help')}
+
+
+
+
+ }
+ />
+ )
+}
+
+export default withRole(OrgUserSettingsCard, nonGovRoles)
diff --git a/frontend/src/views/Dashboard/components/cards/bceid/__tests__/OrgUserSettingsCard.test.jsx b/frontend/src/views/Dashboard/components/cards/bceid/__tests__/OrgUserSettingsCard.test.jsx
new file mode 100644
index 000000000..3a4aa4030
--- /dev/null
+++ b/frontend/src/views/Dashboard/components/cards/bceid/__tests__/OrgUserSettingsCard.test.jsx
@@ -0,0 +1,160 @@
+import React from 'react'
+import { render, screen, fireEvent } from '@testing-library/react'
+import { vi, describe, it, expect, beforeEach } from 'vitest'
+import OrgUserSettingsCard from '../OrgUserSettingsCard'
+import { useCurrentUser } from '@/hooks/useCurrentUser'
+import { useNavigate } from 'react-router-dom'
+import { ROUTES } from '@/constants/routes'
+import { wrapper } from '@/tests/utils/wrapper'
+
+vi.mock('@/hooks/useCurrentUser')
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key) => `mock__${key}`
+ })
+}))
+
+vi.mock('react-router-dom', () => ({
+ ...vi.importActual('react-router-dom'),
+ useNavigate: vi.fn()
+}))
+
+vi.mock('@/utils/withRole', () => ({
+ __esModule: true,
+ default: (Component) =>
+ function MockWithRole(props) {
+ return
+ }
+}))
+
+describe('OrgUserSettingsCard', () => {
+ const mockNavigate = vi.fn()
+
+ beforeEach(() => {
+ mockNavigate.mockClear()
+ useNavigate.mockReturnValue(mockNavigate)
+ })
+
+ it('renders the user’s full name and title correctly', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: 'Developer',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ expect(screen.getByText('Test User, Developer')).toBeInTheDocument()
+
+ expect(
+ screen.getByText('mock__dashboard:orgUserSettings.notifications')
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText('mock__dashboard:orgUserSettings.configureNotifications')
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText('mock__dashboard:orgUserSettings.help')
+ ).toBeInTheDocument()
+ })
+
+ it('handles missing title gracefully', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: undefined,
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ // Should be "Test User" with no comma
+ expect(screen.getByText('Test User')).toBeInTheDocument()
+ })
+
+ it('handles missing firstName or lastName gracefully', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: '',
+ title: 'Developer',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ // Should show "Test, Developer"
+ expect(screen.getByText('Test, Developer')).toBeInTheDocument()
+ })
+
+ it('navigates to Notifications page when "mock__dashboard:orgUserSettings.notifications" is clicked', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: 'Dev',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ const notificationsLink = screen.getByText(
+ 'mock__dashboard:orgUserSettings.notifications'
+ )
+ fireEvent.click(notificationsLink)
+
+ expect(mockNavigate).toHaveBeenCalledWith(ROUTES.NOTIFICATIONS)
+ })
+
+ it('navigates to Notifications Settings page when "mock__dashboard:orgUserSettings.configureNotifications" is clicked', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: 'Dev',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ const configureLink = screen.getByText(
+ 'mock__dashboard:orgUserSettings.configureNotifications'
+ )
+ fireEvent.click(configureLink)
+
+ expect(mockNavigate).toHaveBeenCalledWith(ROUTES.NOTIFICATIONS_SETTINGS)
+ })
+
+ it('navigates to the help page (placeholder) when "mock__dashboard:orgUserSettings.help" is clicked', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: 'Dev',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ const helpLink = screen.getByText('mock__dashboard:orgUserSettings.help')
+ fireEvent.click(helpLink)
+
+ // By default, it calls navigate() with no args
+ expect(mockNavigate).toHaveBeenCalled()
+ })
+})
diff --git a/frontend/src/views/Dashboard/components/cards/idir/UserSettingsCard.jsx b/frontend/src/views/Dashboard/components/cards/idir/UserSettingsCard.jsx
new file mode 100644
index 000000000..76331bb0d
--- /dev/null
+++ b/frontend/src/views/Dashboard/components/cards/idir/UserSettingsCard.jsx
@@ -0,0 +1,79 @@
+import React from 'react'
+import { List, ListItemButton, Stack, Typography } from '@mui/material'
+import { useNavigate } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
+import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard'
+import BCTypography from '@/components/BCTypography'
+import withRole from '@/utils/withRole'
+import { govRoles } from '@/constants/roles'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
+import { useCurrentUser } from '@/hooks/useCurrentUser'
+import { ROUTES } from '@/constants/routes'
+
+const linkStyle = {
+ textDecoration: 'underline',
+ color: 'link.main',
+ '&:hover': { color: 'info.main' }
+}
+
+const UserSettingsLink = ({ onClick, children }) => (
+
+
+ {children}
+
+
+)
+
+const UserSettingsCard = () => {
+ const { t } = useTranslation(['dashboard'])
+ const { data: currentUser } = useCurrentUser()
+ const navigate = useNavigate()
+
+ const { title, firstName, lastName } = currentUser || {}
+ const name = [firstName, lastName].filter(Boolean).join(' ')
+ const displayName = [name, title].filter(Boolean).join(', ')
+
+ return (
+
+
+ {displayName}
+
+
+
+ navigate(ROUTES.NOTIFICATIONS)}>
+ {t('dashboard:userSettings.notifications')}
+
+
+ navigate(ROUTES.NOTIFICATIONS_SETTINGS)}
+ >
+ {t('dashboard:userSettings.configureNotifications')}
+
+
+ {/* TODO: Update the link to the help page */}
+ navigate()}>
+ {t('dashboard:userSettings.help')}
+
+
+
+
+ }
+ />
+ )
+}
+
+export default withRole(UserSettingsCard, govRoles)
diff --git a/frontend/src/views/Dashboard/components/cards/idir/__tests__/UserSettingsCard.test.jsx b/frontend/src/views/Dashboard/components/cards/idir/__tests__/UserSettingsCard.test.jsx
new file mode 100644
index 000000000..cf0b93b21
--- /dev/null
+++ b/frontend/src/views/Dashboard/components/cards/idir/__tests__/UserSettingsCard.test.jsx
@@ -0,0 +1,160 @@
+import React from 'react'
+import { render, screen, fireEvent } from '@testing-library/react'
+import { vi, describe, it, expect, beforeEach } from 'vitest'
+import UserSettingsCard from '../UserSettingsCard'
+import { useCurrentUser } from '@/hooks/useCurrentUser'
+import { useNavigate } from 'react-router-dom'
+import { ROUTES } from '@/constants/routes'
+import { wrapper } from '@/tests/utils/wrapper'
+
+vi.mock('@/hooks/useCurrentUser')
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key) => `mock__${key}`
+ })
+}))
+
+vi.mock('react-router-dom', () => ({
+ ...vi.importActual('react-router-dom'),
+ useNavigate: vi.fn()
+}))
+
+vi.mock('@/utils/withRole', () => ({
+ __esModule: true,
+ default: (Component) =>
+ function MockWithRole(props) {
+ return
+ }
+}))
+
+describe('UserSettingsCard', () => {
+ const mockNavigate = vi.fn()
+
+ beforeEach(() => {
+ mockNavigate.mockClear()
+ useNavigate.mockReturnValue(mockNavigate)
+ })
+
+ it('renders the user’s full name and title correctly', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: 'Developer',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ expect(screen.getByText('Test User, Developer')).toBeInTheDocument()
+
+ expect(
+ screen.getByText('mock__dashboard:userSettings.notifications')
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText('mock__dashboard:userSettings.configureNotifications')
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText('mock__dashboard:userSettings.help')
+ ).toBeInTheDocument()
+ })
+
+ it('handles missing title gracefully', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: undefined,
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ // Should be "Test User" with no comma
+ expect(screen.getByText('Test User')).toBeInTheDocument()
+ })
+
+ it('handles missing firstName or lastName gracefully', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: '',
+ title: 'Developer',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ // Should show "Test, Developer"
+ expect(screen.getByText('Test, Developer')).toBeInTheDocument()
+ })
+
+ it('navigates to Notifications page when "mock__dashboard:userSettings.notifications" is clicked', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: 'Dev',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ const notificationsLink = screen.getByText(
+ 'mock__dashboard:userSettings.notifications'
+ )
+ fireEvent.click(notificationsLink)
+
+ expect(mockNavigate).toHaveBeenCalledWith(ROUTES.NOTIFICATIONS)
+ })
+
+ it('navigates to Notifications Settings page when "mock__dashboard:userSettings.configureNotifications" is clicked', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: 'Dev',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ const configureLink = screen.getByText(
+ 'mock__dashboard:userSettings.configureNotifications'
+ )
+ fireEvent.click(configureLink)
+
+ expect(mockNavigate).toHaveBeenCalledWith(ROUTES.NOTIFICATIONS_SETTINGS)
+ })
+
+ it('navigates to the help page (placeholder) when "mock__dashboard:userSettings.help" is clicked', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ firstName: 'Test',
+ lastName: 'User',
+ title: 'Dev',
+ roles: [{ name: 'Government' }]
+ },
+ isLoading: false
+ })
+
+ render(, { wrapper })
+
+ const helpLink = screen.getByText('mock__dashboard:userSettings.help')
+ fireEvent.click(helpLink)
+
+ // By default, it calls navigate() with no args
+ expect(mockNavigate).toHaveBeenCalled()
+ })
+})
diff --git a/frontend/src/views/Dashboard/components/cards/index.js b/frontend/src/views/Dashboard/components/cards/index.js
index 58aefc825..95f57fb9e 100644
--- a/frontend/src/views/Dashboard/components/cards/index.js
+++ b/frontend/src/views/Dashboard/components/cards/index.js
@@ -2,6 +2,7 @@
export { default as AdminLinksCard } from './idir/AdminLinksCard'
export { default as DirectorReviewCard } from './idir/DirectorReviewCard'
export { default as TransactionsCard } from './idir/TransactionsCard'
+export { default as UserSettingsCard } from './idir/UserSettingsCard'
// BCeID Cards
export { default as OrgDetailsCard } from './bceid/OrgDetailsCard'
@@ -10,3 +11,4 @@ export { default as FeedbackCard } from './bceid/FeedbackCard'
export { default as WebsiteCard } from './bceid/WebsiteCard'
export { default as OrgTransactionsCard } from './bceid/OrgTransactionsCard'
export { default as OrgComplianceReportsCard } from './bceid/OrgComplianceReportsCard'
+export { default as OrgUserSettingsCard } from './bceid/OrgUserSettingsCard'