diff --git a/src/atoms/Icons/MapIndicator.tsx b/src/atoms/Icons/MapIndicator.tsx
new file mode 100644
index 00000000..9744e9e8
--- /dev/null
+++ b/src/atoms/Icons/MapIndicator.tsx
@@ -0,0 +1,15 @@
+import { Base, BaseProps } from './Base'
+
+export function MapIndicator(props: BaseProps): JSX.Element {
+ return (
+
+
+
+ )
+}
+
+MapIndicator.displayName = 'MapIndicator'
diff --git a/src/atoms/Icons/index.tsx b/src/atoms/Icons/index.tsx
index b5c97b41..72968ed7 100644
--- a/src/atoms/Icons/index.tsx
+++ b/src/atoms/Icons/index.tsx
@@ -8,8 +8,9 @@ export * from './CircularInformation'
export * from './Download'
export * from './GoAhead'
export * from './GoBack'
-export * from './Multimedia'
export * from './Loader'
+export * from './MapIndicator'
+export * from './Multimedia'
export * from './Profile'
export * from './Remote'
export * from './Schedule'
diff --git a/src/documentation/pages/Organisms/CalendarDropdown.tsx b/src/documentation/pages/Organisms/CalendarDropdown.tsx
index e69a5d92..8b992fea 100644
--- a/src/documentation/pages/Organisms/CalendarDropdown.tsx
+++ b/src/documentation/pages/Organisms/CalendarDropdown.tsx
@@ -62,9 +62,10 @@ interface ICalendarDropdown {
onClickEvent
- El prop onClickEvent es opcional. Si se envía, se ejecuta al presionar
- cualquiera de los eventos del dropdown y entrega hacia arriba la información completa del
- evento seleccionado.
+ El prop onClickEvent es opcional. Si se envía, se ejecuta al presionar un
+ evento del dropdown siempre que ese evento también tenga url , y entrega
+ hacia arriba la información completa del evento seleccionado. Los eventos sin URL se
+ muestran como no disponibles y no ejecutan el callback.
{
Dentro del menú dropdown y las modales que se abren en la vista calendario, se muestra el
detalle del nombre del curso al que pertenece el evento.
+ Los eventos de tipo cpr muestran la dirección de la sede recibida en{' '}
+ headquartersAddress . Si este dato no llega, no se muestra ni el texto de
+ sede ni el ícono de ubicación.
+
Para esto, el componente debe recibir los props{' '}
@@ -68,6 +72,12 @@ export const EventsListPage = (): JSX.Element => {
+
+ El evento solo se comporta como clickeable cuando recibe tanto onClick como{' '}
+ url . Si url llega vacío o nulo, se muestra el estado{' '}
+ Aún no disponible y se deshabilita la interacción.
+
+
{
courseName="[Pruebas TI] - Herramientas para la Gestión Estratégica de Procesos"
duration={40}
/>
+
3. Eventos en vista curso
@@ -121,13 +145,14 @@ export const EventsListPage = (): JSX.Element => {
void // Permite usar el item como elemento clickeable
showCourse?: boolean // Indica si se muestra el curso
showUnit?: boolean // Indica si se muestra la unidad
@@ -135,6 +160,8 @@ interface IEventList {
unitName?: string // Nombre de la unidad
text: string // "Curso"
type: string // Identificador del tipo de evento
+ unavailableLabel?: string // Texto del estado no disponible
+ url?: string | null // URL requerida junto a onClick para habilitar interacción
}
`}
/>
diff --git a/src/organisms/Calendar/Dropdown/CalendarDropdown/Components/EventsGroup.test.tsx b/src/organisms/Calendar/Dropdown/CalendarDropdown/Components/EventsGroup.test.tsx
new file mode 100644
index 00000000..3132a434
--- /dev/null
+++ b/src/organisms/Calendar/Dropdown/CalendarDropdown/Components/EventsGroup.test.tsx
@@ -0,0 +1,82 @@
+import { ChakraProvider, Menu, MenuList } from '@chakra-ui/react'
+import { render, RenderResult, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+
+import { Event } from '../../types'
+import { EventsGroup } from './EventsGroup'
+
+const baseEvent: Event = {
+ id: 1,
+ associated_resource: {
+ id: 1,
+ name: 'Evaluación 2',
+ },
+ course: {
+ id: 1,
+ name: 'Economía',
+ },
+ course_id: 1,
+ end: '2026-07-27T03:59:00.000Z',
+ start: '2026-07-27T02:59:00.000Z',
+ formatedDate: {
+ day: 'viernes',
+ date: '26 julio',
+ time: '23:59 hrs.',
+ },
+ translatedTitle: 'Se habilita para responder “Evaluación 2”',
+ type: 'evaluation-release',
+ url: null,
+}
+
+const renderComponent = (event: Event, onClickEvent = jest.fn()): RenderResult =>
+ render(
+
+
+
+
+
+
+
+ )
+
+describe('EventsGroup', () => {
+ it('renders unavailable event label and does not call onClickEvent when url is empty', async () => {
+ const user = userEvent.setup()
+ const onClickEvent = jest.fn()
+
+ renderComponent(baseEvent, onClickEvent)
+
+ expect(screen.getByText('Aún no disponible')).toBeInTheDocument()
+
+ await user.click(screen.getByText('Se habilita para responder “Evaluación 2”'))
+
+ expect(onClickEvent).not.toHaveBeenCalled()
+ })
+
+ it('calls onClickEvent when event has url', async () => {
+ const user = userEvent.setup()
+ const onClickEvent = jest.fn()
+ const event = { ...baseEvent, url: 'https://example.com/evaluation-2' }
+
+ renderComponent(event, onClickEvent)
+
+ await user.click(screen.getByText('Se habilita para responder “Evaluación 2”'))
+
+ expect(onClickEvent).toHaveBeenCalledWith(event)
+ })
+
+ it('renders headquarters address for cpr events', () => {
+ const event = {
+ ...baseEvent,
+ duration_in_minutes: 40,
+ headquarters_address: 'Sede Apoquindo',
+ type: 'cpr',
+ url: 'https://example.com/cpr',
+ }
+
+ renderComponent(event)
+
+ expect(screen.getByText('Sede Apoquindo')).toBeInTheDocument()
+ expect(screen.queryByText('Link clase online')).not.toBeInTheDocument()
+ })
+})
diff --git a/src/organisms/Calendar/Dropdown/CalendarDropdown/Components/EventsGroup.tsx b/src/organisms/Calendar/Dropdown/CalendarDropdown/Components/EventsGroup.tsx
index 78aa75ef..e3c0037d 100644
--- a/src/organisms/Calendar/Dropdown/CalendarDropdown/Components/EventsGroup.tsx
+++ b/src/organisms/Calendar/Dropdown/CalendarDropdown/Components/EventsGroup.tsx
@@ -45,17 +45,20 @@ export const EventsGroup = ({
>
{events.map((event: Event) => {
+ const hasUrl = Boolean(event.url)
+ const eventOnClick = onClickEvent && hasUrl ? () => onClickEvent(event) : undefined
+
return (
// Una vez que el evento se comporte como link, se debe cambiar Box a MenuItem y aplicar el efecto de focus
onClickEvent(event) : undefined}
+ headquartersAddress={event.headquarters_address}
+ onClick={eventOnClick}
showCourse
+ url={event.url ?? ''}
/>
)
diff --git a/src/organisms/Calendar/Dropdown/types.d.ts b/src/organisms/Calendar/Dropdown/types.d.ts
index 4db64c14..a8d5f82b 100644
--- a/src/organisms/Calendar/Dropdown/types.d.ts
+++ b/src/organisms/Calendar/Dropdown/types.d.ts
@@ -22,10 +22,12 @@ export interface Event {
end: string
start: string
formatedDate: FormattedDate
+ headquarters_address?: string | null
id: number
isNew?: boolean
translatedTitle: string
type: string
+ url?: string | null
}
export type Events = Event[]
diff --git a/src/organisms/Calendar/EventsList/EventsList.test.tsx b/src/organisms/Calendar/EventsList/EventsList.test.tsx
index 5b1defe8..38652f2a 100644
--- a/src/organisms/Calendar/EventsList/EventsList.test.tsx
+++ b/src/organisms/Calendar/EventsList/EventsList.test.tsx
@@ -32,19 +32,43 @@ describe('EventsList', () => {
const onClick = jest.fn()
const user = userEvent.setup()
- renderComponent(onClick)
+ renderComponent(onClick, { url: '/demo' })
await user.click(screen.getByText('Evento demo'))
expect(onClick).toHaveBeenCalledTimes(1)
})
+ it('does not call onClick when url is not provided', async () => {
+ const onClick = jest.fn()
+ const user = userEvent.setup()
+
+ renderComponent(onClick)
+
+ await user.click(screen.getByText('Evento demo'))
+
+ expect(onClick).not.toHaveBeenCalled()
+ })
+
it('renders the item when onClick is not provided', () => {
renderComponent()
expect(screen.getByText('Evento demo')).toBeInTheDocument()
})
+ it('renders unavailable label and does not call onClick when url is empty', async () => {
+ const onClick = jest.fn()
+ const user = userEvent.setup()
+
+ renderComponent(onClick, { url: '' })
+
+ expect(screen.getByText('Aún no disponible')).toBeInTheDocument()
+
+ await user.click(screen.getByText('Evento demo'))
+
+ expect(onClick).not.toHaveBeenCalled()
+ })
+
it('renders duration for online or in-person events with positive minutes', () => {
const { container } = renderComponent(undefined, { duration: 40 })
@@ -52,6 +76,29 @@ describe('EventsList', () => {
expect(container).toHaveTextContent(/40\s*min/)
})
+ it('renders headquarters address instead of online class link for cpr events', () => {
+ const { container } = renderComponent(undefined, {
+ duration: 40,
+ headquartersAddress: 'Sede Apoquindo',
+ type: 'cpr',
+ })
+
+ expect(screen.getByText('Sede Apoquindo')).toBeInTheDocument()
+ expect(screen.queryByText('Link clase online')).not.toBeInTheDocument()
+ expect(container).toHaveTextContent(/40\s*min/)
+ })
+
+ it('does not render location text when cpr event has no headquarters address', () => {
+ const { container } = renderComponent(undefined, {
+ duration: 40,
+ headquartersAddress: null,
+ type: 'cpr',
+ })
+
+ expect(screen.queryByText('Link clase online')).not.toBeInTheDocument()
+ expect(container).toHaveTextContent(/40\s*min/)
+ })
+
it('does not render duration when minutes are zero', () => {
renderComponent(undefined, { duration: 0 })
diff --git a/src/organisms/Calendar/EventsList/EventsList.tsx b/src/organisms/Calendar/EventsList/EventsList.tsx
index 1d08b286..36a22416 100644
--- a/src/organisms/Calendar/EventsList/EventsList.tsx
+++ b/src/organisms/Calendar/EventsList/EventsList.tsx
@@ -1,4 +1,5 @@
-import { Remote, Time } from '@/atoms/Icons'
+import { TinyAlert } from '@/atoms'
+import { MapIndicator, Remote, Time } from '@/atoms/Icons'
import { Box } from '@chakra-ui/react'
import { vars } from '@theme'
@@ -12,6 +13,7 @@ export interface IEventList {
duration?: number
name: string
hasNotification?: boolean
+ headquartersAddress?: string | null
onClick?: () => void
showCourse?: boolean
showUnit?: boolean
@@ -19,6 +21,8 @@ export interface IEventList {
text: string
type: string
unitName?: string
+ unavailableLabel?: string
+ url?: string | null
}
export const EventsList = ({
@@ -29,6 +33,7 @@ export const EventsList = ({
duration,
name,
hasNotification,
+ headquartersAddress,
onClick,
showCourse,
showUnit,
@@ -36,13 +41,20 @@ export const EventsList = ({
text,
type,
unitName,
+ unavailableLabel = 'Aún no disponible',
+ url,
}: IEventList): JSX.Element => {
const border = `1px solid ${vars('colors-neutral-platinum') ?? '#E8E8E8'}`
const hoverBg = vars('colors-neutral-cultured2') ?? '#F8F8F8'
- const isClickable = Boolean(onClick)
+ const hasUrl = Boolean(url)
+ const isAvailable = url === undefined ? true : hasUrl
+ const isClickable = Boolean(onClick) && hasUrl
+ const disabledOpacity = isAvailable ? 1 : 0.5
+ const isCpr = type === 'cpr'
+ const showEventLocation = !isCpr || Boolean(headquartersAddress)
const showEventDuration =
- ['online', 'in-person'].includes(type) && duration !== undefined && duration > 0
+ ['online', 'in-person', 'cpr'].includes(type) && duration !== undefined && duration > 0
const initOrEnd = [
'end-course',
@@ -82,7 +94,7 @@ export const EventsList = ({
display="flex"
gap="12px"
cursor={isClickable ? 'pointer' : 'default'}
- onClick={onClick}
+ onClick={isClickable ? onClick : undefined}
p="16px"
transition="background-color 0.2s ease"
_hover={isClickable ? { bg: hoverBg } : undefined}
@@ -98,6 +110,7 @@ export const EventsList = ({
display="flex"
flexDirection="column"
justifyContent="space-around"
+ opacity={disabledOpacity}
>
{day}
@@ -109,62 +122,83 @@ export const EventsList = ({
- span': {
- width: 'calc(100% - 12px)',
- },
- '>svg': {
- alignSelf: 'flex-start',
- },
- }}
- >
- {name}
- {hasNotification && }
-
+ {!isAvailable && (
+ span': {
+ fontSize: '12px',
+ },
+ }}
+ />
+ )}
- {showCourse && !initOrEnd && (
-
- {type === 'cv-events' ? <>> : {text ? `${text}:` : 'Curso:'} }{' '}
- {courseName}
+
+ span': {
+ width: 'calc(100% - 12px)',
+ },
+ '>svg': {
+ alignSelf: 'flex-start',
+ },
+ }}
+ >
+ {name}
+ {hasNotification && }
- )}
- {showEventDuration && (
-
-
-
-
-
- Link clase online
-
+ {showCourse && !initOrEnd && (
-
-
+ {type === 'cv-events' ? <>> : {text ? `${text}:` : 'Curso:'} }{' '}
+ {courseName}
+
+ )}
+
+ {showEventDuration && (
+
+ {showEventLocation && (
+
+
+ {isCpr ? (
+
+ ) : (
+
+ )}
+
+ {isCpr ? headquartersAddress : 'Link clase online'}
+
+ )}
+
+
+
+
+ {duration} min
- {duration} min
-
- )}
+ )}
- {showUnit && !initOrEnd && (
-
-
- {unitName}
+ {showUnit && !initOrEnd && (
+
+
+ {unitName}
+
-
- )}
+ )}
+
)