diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/calendar/agenda-block/agenda-block.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/calendar/agenda-block/agenda-block.tsx index a19c9480f..f5d1c7d7b 100644 --- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/calendar/agenda-block/agenda-block.tsx +++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/calendar/agenda-block/agenda-block.tsx @@ -5,7 +5,7 @@ import { Dayjs } from 'dayjs'; import { Box } from '@mui/material'; import { useTranslations } from 'next-intl'; import { BLOCK_COLORS, AgendaBlockVisibility, AgendaBlock } from '../calendar-types'; -import { calculateBlockPosition } from '../calendar-utils'; +import { calculateBlockPosition, BlockOverlapInfo } from '../calendar-utils'; import { useCalendar } from '../calendar-context'; import { EditAgendaDialog } from './dialog/edit-agenda-dialog'; import { ResizeHandles } from './resize-handles'; @@ -19,6 +19,7 @@ interface AgendaBlockProps { draggedPosition: number; draggedDuration?: number; // For bottom-edge resize preview draggedStartTime?: Dayjs; // For top-edge resize preview + overlapInfo?: BlockOverlapInfo; // Layout info for overlapping blocks onDragStartBody: (block: AgendaBlock, startY: number) => void; onDragStartTopEdge: (block: AgendaBlock, startY: number) => void; onDragStartBottomEdge: (block: AgendaBlock, startY: number) => void; @@ -32,6 +33,7 @@ export const AgendaBlockComponent: React.FC = ({ draggedPosition, draggedDuration, draggedStartTime, + overlapInfo, onDragStartBody, onDragStartTopEdge, onDragStartBottomEdge @@ -117,13 +119,29 @@ export const AgendaBlockComponent: React.FC = ({ setEditingBlockId(null); }; + const horizontalPosition = useMemo(() => { + if (!overlapInfo || overlapInfo.totalColumns === 1) { + return { left: 8, right: 8 }; + } + + const columnWidth = 100 / overlapInfo.totalColumns; + const leftPercent = columnWidth * overlapInfo.column; + const rightPercent = 100 - (leftPercent + columnWidth); + const gap = 2; + + return { + left: `calc(${leftPercent}% + ${gap}px)`, + right: `calc(${rightPercent}% + ${gap}px)` + }; + }, [overlapInfo]); + return ( = ({ startTime, endTime } const [dragState, setDragState] = useState(null); const columnRef = useRef(null); + const overlapLayout = useMemo(() => { + return calculateOverlapLayout(agendaBlocks as AgendaBlock[]); + }, [agendaBlocks]); + const { handleMouseMove, handleMouseUp } = useDragHandlers({ dragState, startTime, @@ -193,6 +198,7 @@ export const AgendaColumn: React.FC = ({ startTime, endTime } ? dragState.originalStartTime : undefined } + overlapInfo={overlapLayout.get(block.id)} onDragStartBody={handleDragStartBody} onDragStartTopEdge={handleDragStartTopEdge} onDragStartBottomEdge={handleDragStartBottomEdge} diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/calendar/calendar-utils.ts b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/calendar/calendar-utils.ts index 26f52a688..1e2cb7f91 100644 --- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/calendar/calendar-utils.ts +++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/calendar/calendar-utils.ts @@ -1,5 +1,5 @@ import { Dayjs } from 'dayjs'; -import { ScheduleBlock, ScheduleColumn, TIME_SLOT_HEIGHT } from './calendar-types'; +import { ScheduleBlock, ScheduleColumn, TIME_SLOT_HEIGHT, AgendaBlock } from './calendar-types'; export function getBlockColumn(block: ScheduleBlock): ScheduleColumn { if (block.type === 'judging-session') return 'judging'; @@ -18,3 +18,90 @@ export function calculateBlockPosition(startTime: Dayjs, block: ScheduleBlock) { export function getDuration(date: Dayjs): number { return date.hour() * 3600 + date.minute() * 60 + date.second(); } + +export interface BlockOverlapInfo { + column: number; + totalColumns: number; +} + +/** + * Checks if two blocks overlap in time + */ +function blocksOverlap(block1: AgendaBlock, block2: AgendaBlock): boolean { + const end1 = block1.startTime.add(block1.durationSeconds, 'second'); + const end2 = block2.startTime.add(block2.durationSeconds, 'second'); + + return block1.startTime.isBefore(end2) && block2.startTime.isBefore(end1); +} + +/** + * Calculate layout information for overlapping agenda blocks + * Returns a map of block ID to its column position and total columns in its group + */ +export function calculateOverlapLayout(blocks: AgendaBlock[]): Map { + const layout = new Map(); + + const sortedBlocks = [...blocks].sort((a, b) => a.startTime.diff(b.startTime)); + const groups: AgendaBlock[][] = []; + + for (const block of sortedBlocks) { + let foundGroup = false; + + for (const group of groups) { + if (group.some(groupBlock => blocksOverlap(block, groupBlock))) { + group.push(block); + foundGroup = true; + break; + } + } + + if (!foundGroup) { + groups.push([block]); + } + } + + for (const group of groups) { + if (group.length === 1) { + layout.set(group[0].id, { column: 0, totalColumns: 1 }); + } else { + const sortedGroup = [...group].sort((a, b) => { + const timeDiff = a.startTime.diff(b.startTime); + if (timeDiff !== 0) return timeDiff; + return b.durationSeconds - a.durationSeconds; + }); + + const columns: AgendaBlock[][] = []; + + for (const block of sortedGroup) { + let assignedColumn = -1; + + for (let i = 0; i < columns.length; i++) { + const columnBlocks = columns[i]; + const hasOverlap = columnBlocks.some(colBlock => blocksOverlap(block, colBlock)); + + if (!hasOverlap) { + assignedColumn = i; + columnBlocks.push(block); + break; + } + } + + if (assignedColumn === -1) { + assignedColumn = columns.length; + columns.push([block]); + } + + layout.set(block.id, { column: assignedColumn, totalColumns: columns.length }); + } + + for (const block of group) { + const info = layout.get(block.id); + if (info) { + info.totalColumns = columns.length; + } + } + } + } + + return layout; +}