All files / src/components/organizer PartnerMeetingRsvpPanel.tsx

100% Statements 14/14
86.66% Branches 13/15
100% Functions 4/4
100% Lines 12/12

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                                                              1x 6x 4x 2x     1x         6x   6x   3x       6x 1x     5x                                                                                         6x                                              
/**
 * PartnerMeetingRsvpPanel
 * Story 10.27: iCal RSVP Tracking — AC8
 *
 * Displays RSVP responses for a partner meeting invite.
 * Only rendered after an invite has been sent (inviteSentAt non-null).
 * Re-fetches when `refreshKey` changes (incremented by parent after send-invite).
 */
 
import React from 'react';
import {
  Alert,
  Box,
  Chip,
  CircularProgress,
  Divider,
  List,
  ListItem,
  ListItemText,
  Typography,
} from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { getRsvps, type RsvpDTO } from '@/services/api/partnerMeetingsApi';
 
interface PartnerMeetingRsvpPanelProps {
  meetingId: string;
  inviteSentAt: string | null;
  refreshKey?: number;
}
 
const statusColor = (status: RsvpDTO['status']): 'success' | 'error' | 'warning' => {
  if (status === 'ACCEPTED') return 'success';
  if (status === 'DECLINED') return 'error';
  return 'warning';
};
 
const PartnerMeetingRsvpPanel: React.FC<PartnerMeetingRsvpPanelProps> = ({
  meetingId,
  inviteSentAt,
  refreshKey = 0,
}) => {
  const { t } = useTranslation('partners');
 
  const { data, isLoading, isError } = useQuery({
    queryKey: ['meetingRsvps', meetingId, refreshKey],
    queryFn: () => getRsvps(meetingId),
    enabled: inviteSentAt !== null,
  });
 
  if (inviteSentAt === null) {
    return null;
  }
 
  return (
    <Box sx={{ mt: 2 }} data-testid={`rsvp-panel-${meetingId}`}>
      <Divider sx={{ mb: 2 }} />
 
      <Typography variant="subtitle2" gutterBottom>
        {t('meetings.rsvp.title')}
      </Typography>
 
      {isLoading && (
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }} data-testid="rsvp-loading">
          <CircularProgress size={16} />
          <Typography variant="body2" color="text.secondary">
            {t('meetings.rsvp.loading')}
          </Typography>
        </Box>
      )}
 
      {isError && (
        <Alert severity="error" data-testid="rsvp-error">
          {t('meetings.rsvp.error')}
        </Alert>
      )}
 
      {data && (
        <>
          <Typography
            variant="body2"
            color="text.secondary"
            sx={{ mb: 1 }}
            data-testid="rsvp-summary"
          >
            {t('meetings.rsvp.summary', {
              accepted: data.summary.accepted,
              declined: data.summary.declined,
              tentative: data.summary.tentative,
            })}
          </Typography>
 
          {data.rsvps.length === 0 ? (
            <Typography variant="body2" color="text.secondary">
              {t('meetings.rsvp.noResponses')}
            </Typography>
          ) : (
            <List dense disablePadding>
              {data.rsvps.map((rsvp) => (
                <ListItem key={rsvp.attendeeEmail} disableGutters>
                  <Chip
                    label={t(`meetings.rsvp.status.${rsvp.status.toLowerCase()}`)}
                    color={statusColor(rsvp.status)}
                    size="small"
                    sx={{ mr: 1, minWidth: 90 }}
                    data-testid={`rsvp-chip-${rsvp.attendeeEmail}`}
                  />
                  <ListItemText
                    primary={rsvp.attendeeEmail}
                    secondary={new Date(rsvp.respondedAt).toLocaleString()}
                  />
                </ListItem>
              ))}
            </List>
          )}
        </>
      )}
    </Box>
  );
};
 
export default PartnerMeetingRsvpPanel;