@@ -2,15 +2,29 @@ import { Avatar, Button, Chip, Switch, Typography } from "@mui/material/";
22import { XIcon } from "lucide-react" ;
33import { useCallback , useMemo } from "react" ;
44import { useShallow } from "zustand/shallow" ;
5- import { computeGroupMembersForRange } from "@/lib/availability/group-query" ;
6- import { newZonedPageAvailAndDates } from "@/lib/availability/utils" ;
5+ import {
6+ computeGroupMembersForRange ,
7+ type GroupMembersForRange ,
8+ type MemberRangeStatus ,
9+ statusForMember ,
10+ } from "@/lib/availability/group-query" ;
11+ import { cloneDates } from "@/lib/availability/utils" ;
712import type { Member , SelectionStateType } from "@/lib/types/availability" ;
813import { cn } from "@/lib/utils" ;
914import { ZotDate } from "@/lib/zotdate" ;
10- import { useAvailabilityStore } from "@/store/useAvailabilityStore" ;
15+ import {
16+ useActiveSelectionRange ,
17+ useAvailabilityStore ,
18+ } from "@/store/useAvailabilityStore" ;
1119
1220const EN_DASH = "\u2013" ;
1321
22+ const RESPONDER_CHIP_CLASS : Record < MemberRangeStatus , string > = {
23+ available : "" ,
24+ "if-needed" : "[&_.MuiChip-label]:italic" ,
25+ unavailable : "[&_.MuiChip-label]:line-through" ,
26+ } ;
27+
1428/**
1529 * Formats a selection range for display in the sidebar. The range is a
1630 * rectangle in (day, time) space, not a continuous interval, so the day and
@@ -50,42 +64,48 @@ function formatRangeLabel(range: SelectionStateType, dates: ZotDate[]): string {
5064
5165interface GroupResponsesProps {
5266 availabilityDates : ZotDate [ ] ;
67+ ifNeededDates : ZotDate [ ] ;
5368 members : Member [ ] ;
5469 pendingMembers : Member [ ] ;
5570 fromTime : number ;
5671 timezone : string ;
57- anchorNormalizedDate : Date [ ] ;
5872 currentPageAvailability : {
5973 availabilities : ( ZotDate | null ) [ ] ;
6074 ifNeeded : ( ZotDate | null ) [ ] ;
6175 } ;
62- availabilityTimeBlocks : number [ ] ;
6376 doesntNeedDay : boolean ;
6477}
6578
6679export function GroupResponses ( {
6780 availabilityDates,
81+ ifNeededDates,
6882 fromTime,
6983 members,
7084 pendingMembers,
7185 timezone,
7286 currentPageAvailability,
7387 doesntNeedDay,
7488} : GroupResponsesProps ) {
75- const [ _newBlocks , newAvailDates ] = newZonedPageAvailAndDates (
76- currentPageAvailability . availabilities ,
77- availabilityDates ,
78- doesntNeedDay ,
89+ const newAvailDates = useMemo (
90+ ( ) =>
91+ cloneDates (
92+ currentPageAvailability . availabilities ,
93+ availabilityDates ,
94+ doesntNeedDay ,
95+ ) ,
96+ [ currentPageAvailability . availabilities , availabilityDates , doesntNeedDay ] ,
7997 ) ;
80- const [ _newIfNeededBlocks , newIfNeededDates ] = newZonedPageAvailAndDates (
81- currentPageAvailability . ifNeeded ,
82- availabilityDates ,
83- doesntNeedDay ,
98+ const newIfNeededDates = useMemo (
99+ ( ) =>
100+ cloneDates (
101+ currentPageAvailability . ifNeeded ,
102+ ifNeededDates ,
103+ doesntNeedDay ,
104+ ) ,
105+ [ currentPageAvailability . ifNeeded , ifNeededDates , doesntNeedDay ] ,
84106 ) ;
85107
86108 const {
87- draftRange,
88- committedRange,
89109 isMobileDrawerOpen,
90110 setIsMobileDrawerOpen,
91111 setHoveredMember,
@@ -96,8 +116,6 @@ export function GroupResponses({
96116 setEnabled : setShowBestTimes ,
97117 } = useAvailabilityStore (
98118 useShallow ( ( state ) => ( {
99- draftRange : state . draftRange ,
100- committedRange : state . committedRange ,
101119 isMobileDrawerOpen : state . isMobileDrawerOpen ,
102120 setIsMobileDrawerOpen : state . setIsMobileDrawerOpen ,
103121 setHoveredMember : state . setHoveredMember ,
@@ -109,7 +127,7 @@ export function GroupResponses({
109127 } ) ) ,
110128 ) ;
111129
112- const activeRange = draftRange ?? committedRange ;
130+ const activeRange = useActiveSelectionRange ( ) ;
113131
114132 const handleClearSelected = useCallback ( ( ) => {
115133 setSelectedMember ( [ ] ) ;
@@ -129,39 +147,46 @@ export function GroupResponses({
129147 [ toggleSelectedMember ] ,
130148 ) ;
131149
132- const availableMembers = useMemo ( ( ) => {
133- if ( ! activeRange ) return [ ] as Member [ ] ;
134- const { availableMemberIds } = computeGroupMembersForRange ( {
150+ const respondedMembers = useMemo ( ( ) => {
151+ const pendingMemberIds = new Set ( pendingMembers . map ( ( m ) => m . memberId ) ) ;
152+ return members . filter ( ( member ) => ! pendingMemberIds . has ( member . memberId ) ) ;
153+ } , [ members , pendingMembers ] ) ;
154+
155+ const groups = useMemo < GroupMembersForRange | null > ( ( ) => {
156+ if ( ! activeRange ) return null ;
157+ return computeGroupMembersForRange ( {
135158 range : activeRange ,
136159 availabilityDates : newAvailDates ,
137160 ifNeededDates : newIfNeededDates ,
138161 fromTimeMinutes : fromTime ,
139162 timeZone : timezone ,
140163 } ) ;
141- const byId = new Map ( members . map ( ( m ) => [ m . memberId , m ] ) ) ;
142- return availableMemberIds
143- . map ( ( id ) => byId . get ( id ) )
144- . filter ( ( m ) : m is Member => Boolean ( m ) ) ;
145- } , [
146- activeRange ,
147- newAvailDates ,
148- newIfNeededDates ,
149- fromTime ,
150- timezone ,
151- members ,
152- ] ) ;
164+ } , [ activeRange , newAvailDates , newIfNeededDates , fromTime , timezone ] ) ;
153165
154- const respondedMembers = useMemo ( ( ) => {
155- const pendingMemberIds = new Set ( pendingMembers . map ( ( m ) => m . memberId ) ) ;
156- return members . filter ( ( member ) => ! pendingMemberIds . has ( member . memberId ) ) ;
157- } , [ members , pendingMembers ] ) ;
166+ const memberStatus = useMemo < ReadonlyMap < string , MemberRangeStatus > > ( ( ) => {
167+ if ( ! groups ) return new Map ( ) ;
168+ const map = new Map < string , MemberRangeStatus > ( ) ;
169+ for ( const m of respondedMembers ) {
170+ map . set ( m . memberId , statusForMember ( m . memberId , groups ) ) ;
171+ }
172+ return map ;
173+ } , [ groups , respondedMembers ] ) ;
174+
175+ const availableCount = activeRange
176+ ? respondedMembers . filter (
177+ ( m ) => memberStatus . get ( m . memberId ) !== "unavailable" ,
178+ ) . length
179+ : respondedMembers . length ;
158180
159181 const blockInfoString = activeRange
160182 ? formatRangeLabel ( activeRange , newAvailDates )
161183 : "Select a cell to view" ;
162184
163185 return (
164- < div className = "flex min-h-0 min-w-0 flex-1 flex-col lg:shrink-0" >
186+ < div
187+ data-availability-sidebar = ""
188+ className = "flex min-h-0 min-w-0 flex-1 flex-col lg:shrink-0"
189+ >
165190 < div
166191 className = { cn (
167192 // Cap height so the flex row does not grow with responder count (see availability layout).
@@ -221,43 +246,39 @@ export function GroupResponses({
221246 < div >
222247 < h2 className = "font-medium text-xl" > Responders</ h2 >
223248 < Typography variant = "caption" color = "textSecondary" >
224- Available (
225- { activeRange ? availableMembers . length : respondedMembers . length } )
249+ Available ({ availableCount } )
226250 </ Typography >
227251 </ div >
228252
229253 < ul className = "mt-3 flex flex-wrap gap-2" >
230- { respondedMembers . map ( ( member ) => (
231- < Chip
232- key = { member . memberId }
233- clickable
234- icon = {
235- < Avatar
236- alt = { member . displayName }
237- src = { member . profilePicture ?? undefined }
238- slotProps = { { img : { referrerPolicy : "no-referrer" } } }
239- sx = { { width : 24 , height : 24 , fontSize : 12 } }
240- />
241- }
242- label = { member . displayName }
243- color = {
244- selectedMembers . includes ( member . memberId )
245- ? "primary"
246- : "default"
247- }
248- variant = "outlined"
249- sx = {
250- activeRange
251- ? availableMembers . includes ( member )
252- ? { maxWidth : "100%" }
253- : { textDecoration : "line-through" , maxWidth : "100%" }
254- : { maxWidth : "100%" }
255- }
256- onMouseEnter = { ( ) => handleMemberHover ( member . memberId ) }
257- onMouseLeave = { ( ) => handleMemberHover ( null ) }
258- onClick = { ( ) => handleMemberSelect ( member . memberId ) }
259- />
260- ) ) }
254+ { respondedMembers . map ( ( member ) => {
255+ const status = memberStatus . get ( member . memberId ) ?? "available" ;
256+ return (
257+ < Chip
258+ key = { member . memberId }
259+ clickable
260+ icon = {
261+ < Avatar
262+ alt = { member . displayName }
263+ src = { member . profilePicture ?? undefined }
264+ slotProps = { { img : { referrerPolicy : "no-referrer" } } }
265+ sx = { { width : 24 , height : 24 , fontSize : 12 } }
266+ />
267+ }
268+ label = { member . displayName }
269+ color = {
270+ selectedMembers . includes ( member . memberId )
271+ ? "primary"
272+ : "default"
273+ }
274+ variant = "outlined"
275+ className = { cn ( "max-w-full" , RESPONDER_CHIP_CLASS [ status ] ) }
276+ onMouseEnter = { ( ) => handleMemberHover ( member . memberId ) }
277+ onMouseLeave = { ( ) => handleMemberHover ( null ) }
278+ onClick = { ( ) => handleMemberSelect ( member . memberId ) }
279+ />
280+ ) ;
281+ } ) }
261282 </ ul >
262283 < div className = "mt-4 ml-auto" >
263284 < Button
0 commit comments