All files / src/components/organizer/EventPage EventPublishingTab.tsx

100% Statements 16/16
90% Branches 27/30
100% Functions 3/3
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162                                              1x 11x 11x 11x     11x 1x                         10x   11x 11x       11x           11x 11x   11x                                                   11x                                                                                                                                                        
/**
 * EventPublishingTab Component (Story 5.7 - Updated)
 *
 * Progressive publishing controls with validation, timeline, and version control
 * Now uses real data from usePublishing hook instead of mocked data
 */
 
import React from 'react';
import { Stack, Skeleton, Box, Typography, Paper } from '@mui/material';
import { useTranslation } from 'react-i18next';
import type { Event, EventDetailUI, PublishingPhase } from '@/types/event.types';
import { ValidationDashboard } from '@/components/Publishing/ValidationDashboard/ValidationDashboard';
import { PublishingControls } from '@/components/Publishing/PublishingControls/PublishingControls';
import { LivePreview } from '@/components/Publishing/LivePreview/LivePreview';
import { PublishingTimeline } from '@/components/Publishing/PublishingTimeline/PublishingTimeline';
import { usePublishing } from '@/hooks/usePublishing/usePublishing';
import { useSlotAssignment } from '@/hooks/useSlotAssignment/useSlotAssignment';
 
interface EventPublishingTabProps {
  event: Event | EventDetailUI;
  eventCode: string;
}
 
export const EventPublishingTab: React.FC<EventPublishingTabProps> = ({ event, eventCode }) => {
  const { t } = useTranslation('events');
  const { publishingStatus, isLoadingStatus, validationErrors } = usePublishing(eventCode);
  const { unassignedSessions } = useSlotAssignment(eventCode);
 
  // Loading state
  if (isLoadingStatus) {
    return (
      <Stack spacing={3}>
        <Skeleton variant="rectangular" height={200} />
        <Skeleton variant="rectangular" height={150} />
        <Skeleton variant="rectangular" height={100} />
        <Skeleton variant="rectangular" height={300} />
      </Stack>
    );
  }
 
  // Extract data from status response, with fallbacks
  // Convert uppercase phase from API to lowercase (API returns 'SPEAKERS', we need 'speakers')
  const currentPhase: PublishingPhase =
    (publishingStatus?.currentPhase?.toLowerCase() as PublishingPhase) || 'topic';
  const publishedPhases: PublishingPhase[] =
    publishingStatus?.publishedPhases?.map((p) => p.toLowerCase() as PublishingPhase) || [];
  const eventDate = event.date || new Date().toISOString();
 
  // Build validation data from status response and slot assignment
  const mappedUnassignedSessions =
    unassignedSessions?.map((session) => ({
      sessionSlug: session.sessionSlug,
      title: session.title || session.sessionSlug,
    })) || [];
 
  // Frontend safeguard: Check if event actually has a topic
  const hasTopicCode = 'topicCode' in event && event.topicCode;
  const topicValidation = publishingStatus?.topic || { isValid: true, errors: [] };
 
  const validationData = {
    topic: {
      ...topicValidation,
      // Override if backend incorrectly reports valid but no topicCode exists
      isValid: !!(topicValidation.isValid && hasTopicCode),
      errors:
        !hasTopicCode && topicValidation.isValid
          ? ['Event topic must be defined']
          : topicValidation.errors,
    },
    speakers: publishingStatus?.speakers || { isValid: true, errors: [] },
    sessions: {
      ...(publishingStatus?.sessions || {
        isValid: true,
        errors: [],
        assignedCount: 0,
        totalCount: 0,
      }),
      // Override isValid: only valid if no unassigned sessions AND there are sessions total
      isValid:
        mappedUnassignedSessions.length === 0 && (publishingStatus?.sessions?.totalCount || 0) > 0,
      // Use actual unassigned sessions from slot assignment hook
      unassignedSessions: mappedUnassignedSessions,
    },
  };
 
  return (
    <Stack spacing={3}>
      {/* Top row: Validation · Publishing phases · Publish actions. On desktop a
          40/40/20 three-column grid (publish buttons stay beside the phases); on
          mobile the columns stack full-width (Phase G — three columns don't fit a phone). */}
      <Box
        sx={{
          display: 'grid',
          gridTemplateColumns: {
            xs: '1fr',
            md: 'minmax(0, 2fr) minmax(0, 2fr) minmax(0, 1fr)',
          },
          gap: 3,
          alignItems: 'stretch',
        }}
      >
        {/* Validation Dashboard - Shows content validation status */}
        <Box
          data-testid="validation-dashboard-container"
          sx={{ display: 'flex', flexDirection: 'column' }}
        >
          <Typography variant="subtitle2" gutterBottom>
            {t('publishing.sections.validation', 'Validation')}
          </Typography>
          <ValidationDashboard
            eventCode={eventCode}
            phase={currentPhase}
            validation={validationData}
          />
        </Box>
 
        {/* Publishing Timeline - Visual timeline of phases */}
        <Box
          data-testid="publishing-timeline-container"
          sx={{ display: 'flex', flexDirection: 'column' }}
        >
          <Typography variant="subtitle2" gutterBottom>
            {t('publishing.sections.phases', 'Publishing phase')}
          </Typography>
          <Paper sx={{ p: 2, flexGrow: 1 }}>
            <PublishingTimeline
              eventCode={eventCode}
              currentPhase={currentPhase}
              publishedPhases={publishedPhases}
              eventDate={eventDate}
            />
          </Paper>
        </Box>
 
        {/* Publishing Controls - phase publish buttons, stacked vertically */}
        <Box
          data-testid="publishing-controls-container"
          sx={{ display: 'flex', flexDirection: 'column' }}
        >
          <Typography variant="subtitle2" gutterBottom>
            {t('publishing.sections.publishNext', 'Publish next phase')}
          </Typography>
          <Paper sx={{ p: 2, flexGrow: 1 }}>
            <PublishingControls
              eventCode={eventCode}
              currentPhase={currentPhase}
              validationErrors={validationErrors}
            />
          </Paper>
        </Box>
      </Box>
 
      {/* Live Preview - full width below the three columns */}
      <Box data-testid="live-preview-container">
        <LivePreview eventCode={eventCode} phase={currentPhase} />
      </Box>
    </Stack>
  );
};
 
export default EventPublishingTab;