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{' '} + + 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 && ( - - + )} + + {showEventDuration && ( + + {showEventLocation && ( + + + {isCpr ? ( + + ) : ( + + )} + + {isCpr ? headquartersAddress : 'Link clase online'} + + )} + + + + {duration} min - {duration} min - - )} + )} - {showUnit && !initOrEnd && ( - - - {unitName} + {showUnit && !initOrEnd && ( + + + {unitName} + - - )} + )} + )