Skip to content

Commit 6dc6645

Browse files
committed
Add invites v52
1 parent 1f57bcd commit 6dc6645

27 files changed

Lines changed: 191 additions & 70 deletions

File tree

src/components/managementservice/meetings/Meetings.tsx

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import {
1212
DialogContent,
1313
DialogTitle,
1414
FormControl,
15+
FormControlLabel,
1516
InputLabel,
1617
MenuItem,
1718
Select,
19+
Switch,
1820
TextField,
1921
Typography
2022
} from '@mui/material';
23+
import { rrulestr } from 'rrule';
2124
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
2225
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
2326
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
@@ -57,6 +60,7 @@ import {
5760
repeatWeeklyLabel,
5861
repeatCountLabel,
5962
repeatsLabel,
63+
showPastMeetingsLabel,
6064
roomLabel,
6165
scheduleMeetingLabel,
6266
selectButtonLabel,
@@ -99,6 +103,30 @@ const partstatColor = (p?: MeetingPartstat): 'success' | 'error' | 'warning' | '
99103
}
100104
};
101105

106+
// A meeting is "past" only when there is nothing left: no future occurrence AND the last
107+
// occurrence (or single instance) has already ended. Recurring meetings whose last
108+
// occurrence is still in progress are NOT considered past.
109+
const isMeetingPast = (m: Meeting, now: number): boolean => {
110+
const startsAt = Number(m.startsAt);
111+
const endsAt = Number(m.endsAt);
112+
const duration = endsAt - startsAt;
113+
114+
if (!m.rrule) return endsAt < now;
115+
try {
116+
const rule = rrulestr(m.rrule, { dtstart: new Date(startsAt) });
117+
const nextStart = rule.after(new Date(now), true);
118+
119+
if (nextStart) return false;
120+
const lastStart = rule.before(new Date(now), true);
121+
122+
if (!lastStart) return true;
123+
124+
return lastStart.getTime() + duration < now;
125+
} catch {
126+
return endsAt < now;
127+
}
128+
};
129+
102130
const buildRrule = (mode: RepeatMode, interval: number, count: number): string | undefined => {
103131
if (mode === 'NEVER') return undefined;
104132

@@ -159,6 +187,7 @@ const MeetingsTable = () => {
159187
// Set of meetingAttendee.id values that have at least one per-occurrence exception row.
160188
// Used in the table attendees cell to show a trailing '*' hint that exceptions exist.
161189
const [ attendeesWithExceptions, setAttendeesWithExceptions ] = useState<Set<number>>(new Set());
190+
const [ showPastMeetings, setShowPastMeetings ] = useState(false);
162191
const [ attendeeInput, setAttendeeInput ] = useState<User | string | null>(null);
163192
const [ resolveEmail, setResolveEmail ] = useState('');
164193
const [ isResolving, setIsResolving ] = useState(false);
@@ -481,17 +510,30 @@ const MeetingsTable = () => {
481510
}
482511
};
483512

513+
const visibleData = useMemo(() => {
514+
if (showPastMeetings) return data;
515+
const now = Date.now();
516+
517+
return data.filter((m) => !isMeetingPast(m, now));
518+
}, [ data, showPastMeetings ]);
519+
484520
return (
485521
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale={momentLocale}>
486522
<Box>
487523
<Typography variant="h6">{meetingsLabel()}</Typography>
488-
<Button variant="outlined" onClick={handleOpenAdd} sx={{ mt: 1, mb: 1 }} disabled={rooms.length === 0}>
489-
{addNewLabel()}
490-
</Button>
524+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1, mb: 1 }}>
525+
<Button variant="outlined" onClick={handleOpenAdd} disabled={rooms.length === 0}>
526+
{addNewLabel()}
527+
</Button>
528+
<FormControlLabel
529+
control={<Switch checked={showPastMeetings} onChange={(e) => setShowPastMeetings(e.target.checked)} />}
530+
label={showPastMeetingsLabel()}
531+
/>
532+
</Box>
491533
<MaterialReactTable
492534
localization={localization}
493535
columns={columns}
494-
data={data}
536+
data={visibleData}
495537
state={{ isLoading }}
496538
initialState={{
497539
columnVisibility: { id: false },
@@ -537,25 +579,25 @@ const MeetingsTable = () => {
537579
value={description}
538580
onChange={(e) => setDescription(e.target.value)}
539581
/>
540-
<Box sx={{ display: 'flex', gap: 2, mt: 1 }}>
582+
<Box sx={{ display: 'flex', gap: 2, mt: 1, flexWrap: 'wrap' }}>
541583
<DateTimePicker
542584
label={startsAtLabel()}
543585
value={startsAt}
544586
onChange={(v) => setStartsAt(v)}
545587
ampm={false}
546-
sx={{ flex: 1 }}
588+
sx={{ flex: '1 1 240px' }}
547589
/>
548590
<DateTimePicker
549591
label={endsAtLabel()}
550592
value={endsAt}
551593
onChange={(v) => setEndsAt(v)}
552594
ampm={false}
553-
sx={{ flex: 1 }}
595+
sx={{ flex: '1 1 240px' }}
554596
/>
555597
</Box>
556-
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
598+
<Box sx={{ display: 'flex', gap: 2, mt: 2, flexWrap: 'wrap' }}>
557599
<Autocomplete
558-
sx={{ flex: 1 }}
600+
sx={{ flex: '1 1 240px' }}
559601
options={timezoneOptions}
560602
value={timezoneOptions.find((o) => o.value === timezone) ?? undefined}
561603
onChange={(_e, v) => { if (v) setTimezone(v.value); }}
@@ -564,7 +606,7 @@ const MeetingsTable = () => {
564606
disableClearable
565607
renderInput={(params) => <TextField {...params} label={timezoneLabel()} />}
566608
/>
567-
<FormControl sx={{ flex: 1 }}>
609+
<FormControl sx={{ flex: '1 1 240px' }}>
568610
<InputLabel id="meeting-locale-label">{inviteLanguageLabel()}</InputLabel>
569611
<Select
570612
labelId="meeting-locale-label"
@@ -578,8 +620,8 @@ const MeetingsTable = () => {
578620
</Select>
579621
</FormControl>
580622
</Box>
581-
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
582-
<FormControl sx={{ flex: 1 }}>
623+
<Box sx={{ display: 'flex', gap: 2, mt: 2, flexWrap: 'wrap' }}>
624+
<FormControl sx={{ flex: '2 1 200px' }}>
583625
<InputLabel id="meeting-repeat-label">{repeatsLabel()}</InputLabel>
584626
<Select
585627
labelId="meeting-repeat-label"
@@ -599,15 +641,15 @@ const MeetingsTable = () => {
599641
value={repeatInterval}
600642
onChange={(e) => setRepeatInterval(parseInt(e.target.value, 10) || 1)}
601643
disabled={repeatMode === 'NEVER'}
602-
sx={{ width: 120 }}
644+
sx={{ flex: '1 1 120px', minWidth: 120 }}
603645
/>
604646
<TextField
605647
label={repeatCountLabel()}
606648
type="number"
607649
value={repeatCount}
608650
onChange={(e) => setRepeatCount(parseInt(e.target.value, 10) || 1)}
609651
disabled={repeatMode === 'NEVER'}
610-
sx={{ width: 180 }}
652+
sx={{ flex: '1 1 160px', minWidth: 140 }}
611653
/>
612654
</Box>
613655
<Box sx={{ mt: 3 }}>

src/components/managementservice/rooms/RoomMeetings.tsx

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import {
1212
DialogContent,
1313
DialogTitle,
1414
FormControl,
15+
FormControlLabel,
1516
InputLabel,
1617
MenuItem,
1718
Select,
19+
Switch,
1820
TextField,
1921
Typography
2022
} from '@mui/material';
23+
import { rrulestr } from 'rrule';
2124
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
2225
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
2326
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
@@ -59,6 +62,7 @@ import {
5962
repeatWeeklyLabel,
6063
repeatCountLabel,
6164
repeatsLabel,
65+
showPastMeetingsLabel,
6266
scheduleMeetingLabel,
6367
selectButtonLabel,
6468
startsAtLabel,
@@ -104,6 +108,27 @@ const buildRrule = (mode: RepeatMode, interval: number, count: number): string |
104108
return `FREQ=${mode};INTERVAL=${Math.max(1, interval)};COUNT=${Math.max(1, count)}`;
105109
};
106110

111+
const isMeetingPast = (m: Meeting, now: number): boolean => {
112+
const startsAt = Number(m.startsAt);
113+
const endsAt = Number(m.endsAt);
114+
const duration = endsAt - startsAt;
115+
116+
if (!m.rrule) return endsAt < now;
117+
try {
118+
const rule = rrulestr(m.rrule, { dtstart: new Date(startsAt) });
119+
const nextStart = rule.after(new Date(now), true);
120+
121+
if (nextStart) return false;
122+
const lastStart = rule.before(new Date(now), true);
123+
124+
if (!lastStart) return true;
125+
126+
return lastStart.getTime() + duration < now;
127+
} catch {
128+
return endsAt < now;
129+
}
130+
};
131+
107132
const parseRrule = (rrule?: string): { mode: RepeatMode, interval: number, count: number } => {
108133
if (!rrule) return { mode: 'NEVER', interval: 1, count: 10 };
109134
const parts: Record<string, string> = {};
@@ -155,6 +180,7 @@ const RoomMeetingsTable = (props: RoomProp) => {
155180
const [ attendees, setAttendees ] = useState<MeetingAttendee[]>([]);
156181
const [ occurrenceRsvps, setOccurrenceRsvps ] = useState<MeetingOccurrenceRsvp[]>([]);
157182
const [ attendeesWithExceptions, setAttendeesWithExceptions ] = useState<Set<number>>(new Set());
183+
const [ showPastMeetings, setShowPastMeetings ] = useState(false);
158184
const [ attendeeInput, setAttendeeInput ] = useState<User | string | null>(null);
159185
const [ resolveEmail, setResolveEmail ] = useState('');
160186
const [ isResolving, setIsResolving ] = useState(false);
@@ -456,17 +482,30 @@ const RoomMeetingsTable = (props: RoomProp) => {
456482
}
457483
};
458484

485+
const visibleData = useMemo(() => {
486+
if (showPastMeetings) return data;
487+
const now = Date.now();
488+
489+
return data.filter((m) => !isMeetingPast(m, now));
490+
}, [ data, showPastMeetings ]);
491+
459492
return (
460493
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale={momentLocale}>
461494
<Box sx={{ mt: 2 }}>
462495
<Typography variant="h6">{meetingsLabel()}</Typography>
463-
<Button variant="outlined" onClick={handleOpenAdd} sx={{ mt: 1, mb: 1 }}>
464-
{addNewLabel()}
465-
</Button>
496+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1, mb: 1 }}>
497+
<Button variant="outlined" onClick={handleOpenAdd}>
498+
{addNewLabel()}
499+
</Button>
500+
<FormControlLabel
501+
control={<Switch checked={showPastMeetings} onChange={(e) => setShowPastMeetings(e.target.checked)} />}
502+
label={showPastMeetingsLabel()}
503+
/>
504+
</Box>
466505
<MaterialReactTable
467506
localization={localization}
468507
columns={columns}
469-
data={data}
508+
data={visibleData}
470509
state={{ isLoading }}
471510
initialState={{
472511
columnVisibility: { id: false },
@@ -498,25 +537,25 @@ const RoomMeetingsTable = (props: RoomProp) => {
498537
value={description}
499538
onChange={(e) => setDescription(e.target.value)}
500539
/>
501-
<Box sx={{ display: 'flex', gap: 2, mt: 1 }}>
540+
<Box sx={{ display: 'flex', gap: 2, mt: 1, flexWrap: 'wrap' }}>
502541
<DateTimePicker
503542
label={startsAtLabel()}
504543
value={startsAt}
505544
onChange={(v) => setStartsAt(v)}
506545
ampm={false}
507-
sx={{ flex: 1 }}
546+
sx={{ flex: '1 1 240px' }}
508547
/>
509548
<DateTimePicker
510549
label={endsAtLabel()}
511550
value={endsAt}
512551
onChange={(v) => setEndsAt(v)}
513552
ampm={false}
514-
sx={{ flex: 1 }}
553+
sx={{ flex: '1 1 240px' }}
515554
/>
516555
</Box>
517-
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
556+
<Box sx={{ display: 'flex', gap: 2, mt: 2, flexWrap: 'wrap' }}>
518557
<Autocomplete
519-
sx={{ flex: 1 }}
558+
sx={{ flex: '1 1 240px' }}
520559
options={timezoneOptions}
521560
value={timezoneOptions.find((o) => o.value === timezone) ?? undefined}
522561
onChange={(_e, v) => { if (v) setTimezone(v.value); }}
@@ -525,7 +564,7 @@ const RoomMeetingsTable = (props: RoomProp) => {
525564
disableClearable
526565
renderInput={(params) => <TextField {...params} label={timezoneLabel()} />}
527566
/>
528-
<FormControl sx={{ flex: 1 }}>
567+
<FormControl sx={{ flex: '1 1 240px' }}>
529568
<InputLabel id="meeting-locale-label">{inviteLanguageLabel()}</InputLabel>
530569
<Select
531570
labelId="meeting-locale-label"
@@ -539,8 +578,8 @@ const RoomMeetingsTable = (props: RoomProp) => {
539578
</Select>
540579
</FormControl>
541580
</Box>
542-
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
543-
<FormControl sx={{ flex: 1 }}>
581+
<Box sx={{ display: 'flex', gap: 2, mt: 2, flexWrap: 'wrap' }}>
582+
<FormControl sx={{ flex: '2 1 200px' }}>
544583
<InputLabel id="meeting-repeat-label">{repeatsLabel()}</InputLabel>
545584
<Select
546585
labelId="meeting-repeat-label"
@@ -560,15 +599,15 @@ const RoomMeetingsTable = (props: RoomProp) => {
560599
value={repeatInterval}
561600
onChange={(e) => setRepeatInterval(parseInt(e.target.value, 10) || 1)}
562601
disabled={repeatMode === 'NEVER'}
563-
sx={{ width: 120 }}
602+
sx={{ flex: '1 1 120px', minWidth: 120 }}
564603
/>
565604
<TextField
566605
label={repeatCountLabel()}
567606
type="number"
568607
value={repeatCount}
569608
onChange={(e) => setRepeatCount(parseInt(e.target.value, 10) || 1)}
570609
disabled={repeatMode === 'NEVER'}
571-
sx={{ width: 180 }}
610+
sx={{ flex: '1 1 160px', minWidth: 140 }}
572611
/>
573612
</Box>
574613
<Box sx={{ mt: 3 }}>

0 commit comments

Comments
 (0)