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 || ! date || ! slotHour ) return null ;
17+ const key = getSlotAvailabilityKey ( date , slotHour ) ;
18+ return availabilities [ key ] ?? null ;
19+ }
20+
21+ const AVAILABILITY_BADGE : Record <
22+ AvailabilityValue ,
23+ { bg : string ; text : string ; label : string }
24+ > = {
25+ preferred : { bg : "#dcfce7" , text : "#15803d" , label : "★ Preferred" } ,
26+ available : { bg : "#dbeafe" , text : "#1d4ed8" , label : "✓ Available" } ,
27+ unavailable : { bg : "#fee2e2" , text : "#b91c1c" , label : "✗ Unavailable" } ,
28+ } ;
29+
30+ function AvailabilityBadge ( {
31+ value,
32+ } : { value : AvailabilityValue | undefined } ) {
33+ if ( ! value ) return < span style = { { color : "#9ca3af" , fontSize : 11 } } > —</ span > ;
34+ const { bg, text, label } = AVAILABILITY_BADGE [ value ] ;
35+ return (
36+ < span
37+ style = { {
38+ background : bg ,
39+ color : text ,
40+ fontSize : 11 ,
41+ fontWeight : 600 ,
42+ padding : "2px 7px" ,
43+ borderRadius : 999 ,
44+ whiteSpace : "nowrap" ,
45+ } }
46+ >
47+ { label }
48+ </ span >
49+ ) ;
50+ }
51+
52+ function formatDate ( dateStr : string ) {
53+ const d = new Date ( `${ dateStr } T00:00:00` ) ;
54+ return d . toLocaleDateString ( "en-GB" , { month : "short" , day : "numeric" } ) ;
55+ }
56+
57+ function AvailabilityTooltipContent ( {
58+ availabilities,
59+ } : { availabilities : Record < string , string > } ) {
60+ const byDate : Record <
61+ string ,
62+ { am ?: AvailabilityValue ; pm ?: AvailabilityValue }
63+ > = { } ;
64+ for ( const [ key , value ] of Object . entries ( availabilities ) ) {
65+ const [ date , period ] = key . split ( "@" ) ;
66+ if ( ! byDate [ date ] ) byDate [ date ] = { } ;
67+ byDate [ date ] [ period as "am" | "pm" ] = value as AvailabilityValue ;
68+ }
69+ const dates = Object . keys ( byDate ) . sort ( ) ;
70+ if ( dates . length === 0 ) return < span > No availability data</ span > ;
71+
72+ return (
73+ < div style = { { minWidth : 220 , padding : "8px 4px" } } >
74+ < div
75+ style = { {
76+ fontWeight : 700 ,
77+ fontSize : 12 ,
78+ marginBottom : 8 ,
79+ letterSpacing : "0.05em" ,
80+ textTransform : "uppercase" ,
81+ opacity : 0.7 ,
82+ } }
83+ >
84+ Speaker availability
85+ </ div >
86+ < div style = { { display : "flex" , flexDirection : "column" , gap : 6 } } >
87+ { dates . map ( ( date ) => (
88+ < div
89+ key = { date }
90+ style = { {
91+ display : "grid" ,
92+ gridTemplateColumns : "60px 1fr 1fr" ,
93+ alignItems : "center" ,
94+ gap : 8 ,
95+ } }
96+ >
97+ < span style = { { fontSize : 12 , fontWeight : 600 , opacity : 0.85 } } >
98+ { formatDate ( date ) }
99+ </ span >
100+ < AvailabilityBadge value = { byDate [ date ] . am } />
101+ < AvailabilityBadge value = { byDate [ date ] . pm } />
102+ </ div >
103+ ) ) }
104+ </ div >
105+ </ div >
106+ ) ;
107+ }
108+
109+ export const Item = ( { slots, slot, item, rooms, rowStart, date } ) => {
8110 const roomIndexes = item . rooms
9111 . map ( ( room ) => rooms . findIndex ( ( r ) => r . id === room . id ) )
10112 . sort ( ) ;
@@ -41,12 +143,48 @@ export const Item = ({ slots, slot, item, rooms, rowStart }) => {
41143 } }
42144 className = "z-50 bg-slate-200"
43145 >
44- < ScheduleItemCard item = { item } duration = { duration } />
146+ < ScheduleItemCard
147+ item = { item }
148+ duration = { duration }
149+ date = { date }
150+ slotHour = { slot . hour }
151+ />
45152 </ div >
46153 ) ;
47154} ;
48155
49- export const ScheduleItemCard = ( { item, duration } ) => {
156+ function SpeakerNames ( { item } : { item } ) {
157+ const speakerNames = item . speakers . map ( ( s ) => s . fullname ) . join ( ", " ) ;
158+ const availabilities =
159+ item . proposal ?. speaker ?. participant ?. speakerAvailabilities ;
160+ const hasAvailabilities =
161+ availabilities && Object . keys ( availabilities ) . length > 0 ;
162+
163+ if ( ! hasAvailabilities ) {
164+ return < span > { speakerNames } </ span > ;
165+ }
166+
167+ return (
168+ < Tooltip
169+ content = { < AvailabilityTooltipContent availabilities = { availabilities } /> }
170+ >
171+ < span style = { { cursor : "help" , borderBottom : "1px dotted currentColor" } } >
172+ { speakerNames }
173+ </ span >
174+ </ Tooltip >
175+ ) ;
176+ }
177+
178+ export const ScheduleItemCard = ( {
179+ item,
180+ duration,
181+ date = null ,
182+ slotHour = null ,
183+ } ) => {
184+ const availability =
185+ date && slotHour ? getSpeakerAvailability ( item , date , slotHour ) : null ;
186+ const availabilities =
187+ item . proposal ?. speaker ?. participant ?. speakerAvailabilities ?? { } ;
50188 const [ { opacity } , dragRef ] = useDrag (
51189 ( ) => ( {
52190 type : "scheduleItem" ,
@@ -68,6 +206,23 @@ export const ScheduleItemCard = ({ item, duration }) => {
68206
69207 return (
70208 < ul className = "bg-slate-200 p-3" ref = { dragRef } >
209+ { availability === "unavailable" && (
210+ < 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" >
211+ < span > ⚠ Speaker unavailable</ span >
212+ < Tooltip
213+ content = {
214+ < AvailabilityTooltipContent availabilities = { availabilities } />
215+ }
216+ >
217+ < span
218+ 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"
219+ style = { { fontSize : 9 , fontStyle : "italic" , fontFamily : "serif" } }
220+ >
221+ i
222+ </ span >
223+ </ Tooltip >
224+ </ li >
225+ ) }
71226 < li >
72227 [{ item . type } - { duration || "??" } mins]
73228 </ li >
@@ -77,9 +232,7 @@ export const ScheduleItemCard = ({ item, duration }) => {
77232 </ li >
78233 { item . speakers . length > 0 && (
79234 < li >
80- < span >
81- { item . speakers . map ( ( speaker ) => speaker . fullname ) . join ( "," ) }
82- </ span >
235+ < SpeakerNames item = { item } />
83236 </ li >
84237 ) }
85238 < li className = "pt-2" >
0 commit comments