Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 public/locales/locale_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
"ERROR_DASHBOARD_CONFIG_TITLE": "Department Configuration Error",
"ERROR_DEFAULT_MESSAGE": "An unexpected error occurred",
"ERROR_DEFAULT_TITLE": "Error",
"ERROR_LOADING_DASHBOARD": "Error loading dashboard",
"ERROR_NETWORK_MESSAGE": "Unable to connect to the server. Please check your internet connection.",
"ERROR_NETWORK_TITLE": "Network Error",
"ERROR_NO_DEFAULT_DASHBOARD": "No default dashboard configured",
"ERROR_NOT_FOUND_MESSAGE": "The requested resource was not found.",
"ERROR_NOT_FOUND_TITLE": "Not Found",
"ERROR_SERVER_MESSAGE": "The server encountered an error. Please try again later.",
Expand All @@ -47,6 +49,9 @@
"ERROR_VALIDATION_TITLE": "Validation Error",
"EXPANDABLE_TABLE_EMPTY_STATE_MESSAGE": "No data available",
"EXPANDABLE_TABLE_ERROR_MESSAGE": "{{title}}: {{message}}",
"LOADING_CLINICAL_CONFIG": "Loading clinical configuration...",
"LOADING_DASHBOARD_CONFIG": "Loading dashboard configuration...",
"LOADING_DASHBOARD_CONTENT": "Loading dashboard content...",
"NO_ALLERGIES": "No Allergies recorded for this patient.",
"REACTIONS": "Reaction(s)",
"SEVERITY": "Severity"
Expand Down
5 changes: 5 additions & 0 deletions public/locales/locale_es.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
"ERROR_DASHBOARD_CONFIG_TITLE": "Error de configuración del departamento",
"ERROR_DEFAULT_MESSAGE": "Se produjo un error inesperado",
"ERROR_DEFAULT_TITLE": "Error",
"ERROR_LOADING_DASHBOARD": "Error al cargar el panel",
"ERROR_NETWORK_MESSAGE": "No se puede conectar al servidor. Por favor, compruebe su conexión a Internet.",
"ERROR_NETWORK_TITLE": "Error de red",
"ERROR_NO_DEFAULT_DASHBOARD": "No hay panel predeterminado configurado",
"ERROR_NOT_FOUND_MESSAGE": "No se encontró el recurso solicitado.",
"ERROR_NOT_FOUND_TITLE": "No encontrado",
"ERROR_SERVER_MESSAGE": "El servidor encontró un error. Por favor, inténtelo de nuevo más tarde.",
Expand All @@ -47,6 +49,9 @@
"ERROR_VALIDATION_TITLE": "Error de validación",
"EXPANDABLE_TABLE_EMPTY_STATE_MESSAGE": "No hay datos disponibles",
"EXPANDABLE_TABLE_ERROR_MESSAGE": "{{title}}: {{message}}",
"LOADING_CLINICAL_CONFIG": "Cargando configuración clínica...",
"LOADING_DASHBOARD_CONFIG": "Cargando configuración del panel...",
"LOADING_DASHBOARD_CONTENT": "Cargando contenido del panel...",
"NO_ALLERGIES": "No hay alergias registradas para este paciente",
"REACTIONS": "Reacción(es)",
"SEVERITY": "Gravedad"
Expand Down
45 changes: 45 additions & 0 deletions src/components/clinical/dashboardContainer/DashboardContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { Grid, Column, Section } from '@carbon/react';
import { DashboardSectionConfig } from '@types/dashboardConfig';
import DashboardSection from '../dashboardSection/DashboardSection';
import * as styles from './styles/DashboardContainer.module.scss';

export interface DashboardContainerProps {
sections: DashboardSectionConfig[];
activeItemId?: string | null;
}

/**
* DashboardContainer component that renders dashboard sections as Carbon Tiles
*
* @param {DashboardContainerProps} props - Component props
* @returns {React.ReactElement} The rendered component
*/
const DashboardContainer: React.FC<DashboardContainerProps> = ({
sections,
}) => {
// If no sections, show a message
if (!sections.length) {
return <div>No dashboard sections configured</div>;
}

return (
<Section>
<Grid>
{sections.map((section) => (
<Column
lg={16}
md={8}
sm={4}
key={section.name}
className={styles.sectionColumn}
>
<DashboardSection section={section} />
</Column>
))}
</Grid>
</Section>
);
};

export default DashboardContainer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import DashboardContainer from '../DashboardContainer';
import { DashboardSectionConfig as DashboardSectionType } from '@types/dashboardConfig';

// Mock the Carbon components
jest.mock('@carbon/react', () => ({
Grid: jest.fn(({ children }) => (
<div data-testid="carbon-grid">{children}</div>
)),
Column: jest.fn(({ children }) => (
<div data-testid="carbon-column">{children}</div>
)),
Section: jest.fn(({ children }) => (
<div data-testid="carbon-section">{children}</div>
)),
}));

// Mock the DashboardSection component
jest.mock('../../dashboardSection/DashboardSection', () => {
return jest.fn(({ section }) => (
<div data-testid={`mocked-section-${section.name}`}>
Mocked Section: {section.name}
</div>
));
});

describe('DashboardContainer Component', () => {
const mockSections: DashboardSectionType[] = [
{
name: 'Section 1',
icon: 'icon-1',
controls: [],
},
{
name: 'Section 2',
icon: 'icon-2',
controls: [],
},
];

it('renders all sections', () => {
render(<DashboardContainer sections={mockSections} />);

// Check if all sections are rendered
expect(screen.getByTestId('mocked-section-Section 1')).toBeInTheDocument();
expect(screen.getByTestId('mocked-section-Section 2')).toBeInTheDocument();
});

it('renders a message when no sections are provided', () => {
render(<DashboardContainer sections={[]} />);

// Check if the no sections message is rendered
expect(
screen.getByText('No dashboard sections configured'),
).toBeInTheDocument();
});

it('renders with Carbon layout components', () => {
render(<DashboardContainer sections={mockSections} />);

// Check if Carbon components are rendered
expect(screen.getByTestId('carbon-section')).toBeInTheDocument();
expect(screen.getByTestId('carbon-grid')).toBeInTheDocument();
expect(screen.getAllByTestId('carbon-column').length).toBe(2); // One column per section
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@use "@carbon/react/scss/spacing" as *;
@use "@carbon/react/scss/colors" as *;
@use "@carbon/react/scss/theme" as *;

.sectionColumn {
margin-bottom: $spacing-07; // Add margin between sections (32px)

&:last-child {
margin-bottom: 0; // Remove margin from the last section
}
}
44 changes: 44 additions & 0 deletions src/components/clinical/dashboardSection/DashboardSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { Tile } from '@carbon/react';
import { DashboardSectionConfig } from '@types/dashboardConfig';
import * as styles from './styles/DashboardSection.module.scss';
import AllergiesTable from '@/displayControls/allergies/AllergiesTable';
import ConditionsTable from '@/displayControls/conditions/ConditionsTable';
import { useTranslation } from 'react-i18next';

export interface DashboardSectionProps {
section: DashboardSectionConfig;
}

//TODO: Refactor this to depend on Controls configuration
const renderSectionContent = (section: DashboardSectionConfig) => {
switch (section.name) {
case 'Allergies':
return <AllergiesTable />;
case 'Conditions':
return <ConditionsTable />;
default:
return null;
}
};
/**
* DashboardSection component that renders a single dashboard section as a Carbon Tile
*
* @param {DashboardSectionProps} props - Component props
* @returns {React.ReactElement} The rendered component
*/
const DashboardSection: React.FC<DashboardSectionProps> = ({ section }) => {
const { t } = useTranslation();
return (
<div id={`section-${section.name}`}>
<Tile>
<p className={styles.sectionTitle}>
{t(section.translationKey || section.name)}
</p>
{renderSectionContent(section)}
</Tile>
</div>
);
};

export default DashboardSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import DashboardSection from '../DashboardSection';
import { DashboardSectionConfig } from '@types/dashboardConfig';

// Mock the Carbon Tile component
jest.mock('@carbon/react', () => ({
Tile: jest.fn(({ children }) => (
<div className="cds--tile" data-testid="carbon-tile">
{children}
</div>
)),
}));

describe('DashboardSection Component', () => {
const mockSection: DashboardSectionConfig = {
name: 'Test Section',
icon: 'test-icon',
controls: [],
};

it('renders with the correct section name', () => {
render(<DashboardSection section={mockSection} />);

// Check if the section name is rendered
expect(screen.getByText('Test Section')).toBeInTheDocument();
});

it('has the correct id attribute', () => {
const { container } = render(<DashboardSection section={mockSection} />);

// Check if the div has the correct id
const sectionDiv = container.querySelector(
`div[id="section-${mockSection.name}"]`,
);
expect(sectionDiv).not.toBeNull();
});

it('renders a Tile component', () => {
render(<DashboardSection section={mockSection} />);

// Check if a Tile component is rendered
expect(screen.getByTestId('carbon-tile')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@use "@carbon/react/scss/spacing" as *;
@use "@carbon/react/scss/colors" as *;
@use "@carbon/react/scss/theme" as *;

//TODO Use Variable
.sectionTitle {
font-weight: 600;
}
89 changes: 89 additions & 0 deletions src/hooks/__tests__/useSidebarNavigation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { render, screen, act } from '@testing-library/react';
import { useSidebarNavigation, SidebarItem } from '@hooks/useSidebarNavigation';

// Test component to expose hook values
interface TestComponentProps {
items: SidebarItem[];
}

const TestComponent: React.FC<TestComponentProps> = ({ items }) => {
const { activeItemId, handleItemClick } = useSidebarNavigation(items);

return (
<div>
<div data-testid="active-item-id">{activeItemId || 'none'}</div>
<button data-testid="set-item1" onClick={() => handleItemClick('item1')}>
Set Item 1
</button>
<button data-testid="set-item2" onClick={() => handleItemClick('item2')}>
Set Item 2
</button>
</div>
);
};

describe('useSidebarNavigation Hook', () => {
const mockSidebarItems = [
{ id: 'item1', label: 'Item 1', icon: 'icon1' },
{ id: 'item2', label: 'Item 2', icon: 'icon2' },
];

it('should return null activeItemId when items array is empty', () => {
render(<TestComponent items={[]} />);
expect(screen.getByTestId('active-item-id')).toHaveTextContent('none');
});

it('should use first item as default active item when no active item is set', () => {
render(<TestComponent items={mockSidebarItems} />);
expect(screen.getByTestId('active-item-id')).toHaveTextContent('item1');
});

it('should update active item when handleItemClick is called', () => {
render(<TestComponent items={mockSidebarItems} />);

// Initial state should use first item
expect(screen.getByTestId('active-item-id')).toHaveTextContent('item1');

// Update active item
act(() => {
screen.getByTestId('set-item2').click();
});

// Should reflect the new active item
expect(screen.getByTestId('active-item-id')).toHaveTextContent('item2');
});

it('should maintain active item through re-renders', () => {
const { rerender } = render(<TestComponent items={mockSidebarItems} />);

// Set active item
act(() => {
screen.getByTestId('set-item2').click();
});

// Rerender with same props
rerender(<TestComponent items={mockSidebarItems} />);

// Should maintain the active item
expect(screen.getByTestId('active-item-id')).toHaveTextContent('item2');
});

it('should reset to first item if active item no longer exists in items array', () => {
const { rerender } = render(<TestComponent items={mockSidebarItems} />);

// Set active item
act(() => {
screen.getByTestId('set-item2').click();
});

// Should show the new active item
expect(screen.getByTestId('active-item-id')).toHaveTextContent('item2');

// Rerender with modified items where item2 is removed
rerender(<TestComponent items={[mockSidebarItems[0]]} />);

// Should reset to first available item
expect(screen.getByTestId('active-item-id')).toHaveTextContent('item1');
});
});
9 changes: 7 additions & 2 deletions src/hooks/useDashboardConfig.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { useState, useEffect } from 'react';
import { DashboardConfig } from '@types/dashboardConfig';
import { DashboardConfigContextType } from '@types/dashboardConfig';
import { getDashboardConfig } from '@services/configService';
import { getFormattedError } from '@utils/common';
import notificationService from '@services/notificationService';

interface UseDashboardConfigResult {
dashboardConfig: DashboardConfig | null;
isLoading: boolean;
error: Error | null;
}

/**
* Custom hook to fetch and manage dashboard configuration
*
Expand All @@ -13,7 +18,7 @@ import notificationService from '@services/notificationService';
*/
export const useDashboardConfig = (
dashboardURL: string | null,
): DashboardConfigContextType => {
): UseDashboardConfigResult => {
const [dashboardConfig, setDashboardConfig] =
useState<DashboardConfig | null>(null);
const [isLoading, setIsLoading] = useState(false);
Expand Down
Loading