11import { useDrag } from "react-dnd" ;
22
3- import { Button } from "@radix-ui/themes" ;
3+ import { Button , Tooltip } from "@radix-ui/themes" ;
44import { useDjangoAdminEditor } from "../shared/django-admin-editor-modal/context" ;
5+ import type { AvailabilityValue } from "../utils/availability" ;
6+ import { getSlotAvailabilityKey } from "../utils/availability" ;
57import { convertHoursToMinutes } from "../utils/time" ;
68
7- export const Item = ( { slots, slot, item, rooms, rowStart } ) => {
9+ function getSpeakerAvailability (
10+ item ,
11+ date : string ,
12+ slotHour : string ,
13+ ) : AvailabilityValue | null {
14+ const availabilities =
15+ item . proposal ?. speaker ?. participant ?. speakerAvailabilities ;
16+ if ( ! availabilities ) return null ;
17+ return availabilities [ getSlotAvailabilityKey ( date , slotHour ) ] ?? null ;
18+ }
19+
20+ const AVAILABILITY_BADGE : Record <
21+ AvailabilityValue ,
22+ { bg : string ; text : string ; label : string }
23+ > = {
24+ preferred : { bg : "#dcfce7" , text : "#15803d" , label : "★ Preferred" } ,
25+ available : { bg : "#dbeafe" , text : "#1d4ed8" , label : "✓ Available" } ,
26+ unavailable : { bg : "#fee2e2" , text : "#b91c1c" , label : "✗ Unavailable" } ,
27+ } ;
28+
29+ function AvailabilityBadge ( {
30+ value,
31+ } : { value : AvailabilityValue | undefined } ) {
32+ if ( ! value ) return < span style = { { color : "#9ca3af" , fontSize : 11 } } > —</ span > ;
33+ const { bg, text, label } = AVAILABILITY_BADGE [ value ] ;
34+ return (
35+ < span
36+ style = { {
37+ background : bg ,
38+ color : text ,
39+ fontSize : 11 ,
40+ fontWeight : 600 ,
41+ padding : "2px 7px" ,
42+ borderRadius : 999 ,
43+ whiteSpace : "nowrap" ,
44+ } }
45+ >
46+ { label }
47+ </ span >
48+ ) ;
49+ }
50+
51+ function formatDate ( dateStr : string ) {
52+ const d = new Date ( `${ dateStr } T00:00:00` ) ;
53+ return d . toLocaleDateString ( "en-GB" , { month : "short" , day : "numeric" } ) ;
54+ }
55+
56+ function AvailabilityTooltipContent ( {
57+ availabilities,
58+ } : { availabilities : Record < string , string > } ) {
59+ const byDate : Record <
60+ string ,
61+ { am ?: AvailabilityValue ; pm ?: AvailabilityValue }
62+ > = { } ;
63+ for ( const [ key , value ] of Object . entries ( availabilities ) ) {
64+ const [ date , period ] = key . split ( "@" ) ;
65+ if ( ! byDate [ date ] ) byDate [ date ] = { } ;
66+ byDate [ date ] [ period as "am" | "pm" ] = value as AvailabilityValue ;
67+ }
68+ const dates = Object . keys ( byDate ) . sort ( ) ;
69+ if ( dates . length === 0 ) return < span > No availability data</ span > ;
70+
71+ return (
72+ < div style = { { minWidth : 220 , padding : "8px 4px" } } >
73+ < div
74+ style = { {
75+ fontWeight : 700 ,
76+ fontSize : 12 ,
77+ marginBottom : 8 ,
78+ letterSpacing : "0.05em" ,
79+ textTransform : "uppercase" ,
80+ opacity : 0.7 ,
81+ } }
82+ >
83+ Speaker availability
84+ </ div >
85+ < div style = { { display : "flex" , flexDirection : "column" , gap : 6 } } >
86+ { dates . map ( ( date ) => (
87+ < div
88+ key = { date }
89+ style = { {
90+ display : "grid" ,
91+ gridTemplateColumns : "60px 1fr 1fr" ,
92+ alignItems : "center" ,
93+ gap : 8 ,
94+ } }
95+ >
96+ < span style = { { fontSize : 12 , fontWeight : 600 , opacity : 0.85 } } >
97+ { formatDate ( date ) }
98+ </ span >
99+ < AvailabilityBadge value = { byDate [ date ] . am } />
100+ < AvailabilityBadge value = { byDate [ date ] . pm } />
101+ </ div >
102+ ) ) }
103+ </ div >
104+ </ div >
105+ ) ;
106+ }
107+
108+ export const Item = ( { slots, slot, item, rooms, rowStart, date } ) => {
8109 const roomIndexes = item . rooms
9110 . map ( ( room ) => rooms . findIndex ( ( r ) => r . id === room . id ) )
10111 . sort ( ) ;
@@ -41,12 +142,48 @@ export const Item = ({ slots, slot, item, rooms, rowStart }) => {
41142 } }
42143 className = "z-50 bg-slate-200"
43144 >
44- < ScheduleItemCard item = { item } duration = { duration } />
145+ < ScheduleItemCard
146+ item = { item }
147+ duration = { duration }
148+ date = { date }
149+ slotHour = { slot . hour }
150+ />
45151 </ div >
46152 ) ;
47153} ;
48154
49- export const ScheduleItemCard = ( { item, duration } ) => {
155+ function SpeakerNames ( { item } : { item } ) {
156+ const speakerNames = item . speakers . map ( ( s ) => s . fullname ) . join ( ", " ) ;
157+ const availabilities =
158+ item . proposal ?. speaker ?. participant ?. speakerAvailabilities ;
159+ const hasAvailabilities =
160+ availabilities && Object . keys ( availabilities ) . length > 0 ;
161+
162+ if ( ! hasAvailabilities ) {
163+ return < span > { speakerNames } </ span > ;
164+ }
165+
166+ return (
167+ < Tooltip
168+ content = { < AvailabilityTooltipContent availabilities = { availabilities } /> }
169+ >
170+ < span style = { { cursor : "help" , borderBottom : "1px dotted currentColor" } } >
171+ { speakerNames }
172+ </ span >
173+ </ Tooltip >
174+ ) ;
175+ }
176+
177+ export const ScheduleItemCard = ( {
178+ item,
179+ duration,
180+ date = null ,
181+ slotHour = null ,
182+ } ) => {
183+ const availability =
184+ date && slotHour ? getSpeakerAvailability ( item , date , slotHour ) : null ;
185+ const availabilities =
186+ item . proposal ?. speaker ?. participant ?. speakerAvailabilities ?? { } ;
50187 const [ { opacity } , dragRef ] = useDrag (
51188 ( ) => ( {
52189 type : "scheduleItem" ,
@@ -68,6 +205,23 @@ export const ScheduleItemCard = ({ item, duration }) => {
68205
69206 return (
70207 < ul className = "bg-slate-200 p-3" ref = { dragRef } >
208+ { availability === "unavailable" && (
209+ < li className = "mb-2 flex items-center gap-1.5 bg-amber-100 text-amber-800 border border-amber-300 text-xs font-semibold px-2 py-1 rounded" >
210+ < span > ⚠ Speaker unavailable</ span >
211+ < Tooltip
212+ content = {
213+ < AvailabilityTooltipContent availabilities = { availabilities } />
214+ }
215+ >
216+ < span
217+ className = "inline-flex items-center justify-center w-3.5 h-3.5 rounded-full bg-amber-400 text-amber-900 cursor-help leading-none"
218+ style = { { fontSize : 9 , fontStyle : "italic" , fontFamily : "serif" } }
219+ >
220+ i
221+ </ span >
222+ </ Tooltip >
223+ </ li >
224+ ) }
71225 < li >
72226 [{ item . type } - { duration || "??" } mins]
73227 </ li >
@@ -77,9 +231,7 @@ export const ScheduleItemCard = ({ item, duration }) => {
77231 </ li >
78232 { item . speakers . length > 0 && (
79233 < li >
80- < span >
81- { item . speakers . map ( ( speaker ) => speaker . fullname ) . join ( "," ) }
82- </ span >
234+ < SpeakerNames item = { item } />
83235 </ li >
84236 ) }
85237 < li className = "pt-2" >
0 commit comments