@@ -13,7 +13,7 @@ function fmtDate(d) { return `${d.getDate()} ${SHORT_MONTHS[d.getMonth()]} $
function fmtTime ( iso ) { if ( ! iso ) return '' ; const d = new Date ( iso ) ; return d . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } ) ; }
function fmtRange ( s , e ) { return ` ${ fmtTime ( s ) } – ${ fmtTime ( e ) } ` ; }
function toDateIn ( iso ) { return iso ? iso . slice ( 0 , 10 ) : '' ; }
function toTimeIn ( iso ) { return iso ? iso . slice ( 11 , 16 ) : '' ; }
function toTimeIn ( iso ) { if ( ! iso ) return '' ; const d = new Date ( iso ) ; const h = String ( d . getHours ( ) ) . padStart ( 2 , '0' ) , m = d . getMinutes ( ) < 30 ? ' 00 ' : ' 30 ' ; return ` $ { h } : $ { m } ` ; }
function buildISO ( d , t ) { return d & & t ? ` $ { d } T $ { t } : 00 ` : ' ' ; }
function addHours ( iso , h ) { const d = new Date ( iso ) ; d.setMinutes ( d.getMinutes ( ) + h * 60 ) ; return d.toISOString ( ) .slice ( 0 , 19 ) ; }
function sameDay ( a , b ) { return a.getFullYear ( ) = = = b.getFullYear ( ) & & a.getMonth ( ) = = = b.getMonth ( ) & & a.getDate ( ) = = = b.getDate ( ) ; }
@@ -23,7 +23,18 @@ function daysInMonth(y,m){ return new Date(y,m+1,0).getDate(); }
const RESP_LABEL = { going : 'Going' , maybe : 'Maybe' , not _going : 'Not Going' } ;
const RESP_COLOR = { going : '#22c55e' , maybe : '#f59e0b' , not _going : '#ef4444' } ;
// ── Mini Calendar ─────────────────────────────────────────────────────────────
/ / 30 - minute time slots
const TIME_SLOTS = ( ( ) = > {
const s = [ ] ;
for ( let h = 0 ; h < 24 ; h + + ) for ( let m of [ 0 , 30 ] ) {
const hh = String ( h ) .padStart ( 2 , ' 0 ' ) , mm = String ( m ) .padStart ( 2 , ' 0 ' ) ;
const disp = ` $ { h = = = 0 ? 12 : h > 12 ? h - 12 : h } : $ { mm } $ { h < 12 ? ' AM ' : ' PM ' } ` ;
s.push ( { value : ` $ { hh } : $ { mm } ` , label : disp } ) ;
}
return s ;
} ) ( ) ;
/ / ─ ─ Mini Calendar ( desktop ) ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
function MiniCalendar ( { selected , onChange , eventDates = new Set ( ) } ) {
const [ cur , setCur ] = useState ( ( ) = > { const d = new Date ( selected || Date . now ( ) ) ; d . setDate ( 1 ) ; return d ; } ) ;
const y = cur . getFullYear ( ) , m = cur . getMonth ( ) , first = new Date ( y , m , 1 ) . getDay ( ) , total = daysInMonth ( y , m ) , today = new Date ( ) ;
@@ -53,6 +64,45 @@ function MiniCalendar({ selected, onChange, eventDates=new Set() }) {
) ;
}
// ── Mobile Date Picker (accordion month view) ─────────────────────────────────
function MobileDatePicker ( { selected , onChange , eventDates = new Set ( ) } ) {
const [ open , setOpen ] = useState ( false ) ;
const [ cur , setCur ] = useState ( ( ) => { const d = new Date ( selected || Date . now ( ) ) ; d . setDate ( 1 ) ; return d ; } ) ;
const y = cur . getFullYear ( ) , m = cur . getMonth ( ) , first = new Date ( y , m , 1 ) . getDay ( ) , total = daysInMonth ( y , m ) ;
const cells = [ ] ; for ( let i = 0 ; i < first ; i + + ) cells.push ( null ) ; for ( let d = 1 ; d < = total ; d + + ) cells.push ( d ) ;
const today = new Date ( ) ;
return (
< div style = { { borderBottom : '1px solid var(--border)' , background : 'var(--surface)' } } >
< button onClick = { ( ) => setOpen ( v => ! v ) } style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , width : '100%' , padding : '10px 16px' , background : 'none' , border : 'none' , cursor : 'pointer' , fontSize : 14 , fontWeight : 600 , color : 'var(--text-primary)' } } >
< span > { MONTHS [ m ] } { y } < / span >
< span style = { { fontSize : 10 , transform : open ? 'rotate(180deg)' : 'none' , display : 'inline-block' , transition : 'transform 0.2s' } } > ▼ < / span >
< / button >
{ open && (
< div style = { { padding : '8px 12px 12px' , userSelect : 'none' } } >
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , marginBottom : 8 } } >
< button style = { { background : 'none' , border : 'none' , cursor : 'pointer' , padding : '4px 10px' , fontSize : 16 , color : 'var(--text-secondary)' } } onClick = { ( ) => { const n = new Date ( cur ) ; n . setMonth ( m - 1 ) ; setCur ( n ) ; } } > ‹ < / button >
< button style = { { background : 'none' , border : 'none' , cursor : 'pointer' , padding : '4px 10px' , fontSize : 16 , color : 'var(--text-secondary)' } } onClick = { ( ) => { const n = new Date ( cur ) ; n . setMonth ( m + 1 ) ; setCur ( n ) ; } } > › < / button >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(7,1fr)' , gap : 2 , fontSize : 12 } } >
{ DAYS . map ( d => < div key = { d } style = { { textAlign : 'center' , fontWeight : 600 , color : 'var(--text-tertiary)' , padding : '2px 0' } } > { d [ 0 ] } < / div > ) }
{ cells . map ( ( d , i ) => {
if ( ! d ) return < div key = { i } / > ;
const date = new Date ( y , m , d ) , isSel = selected && sameDay ( date , new Date ( selected ) ) , isToday = sameDay ( date , today ) ;
const key = ` ${ y } - ${ String ( m + 1 ) . padStart ( 2 , '0' ) } - ${ String ( d ) . padStart ( 2 , '0' ) } ` ;
return (
< div key = { i } onClick = { ( ) => { onChange ( date ) ; setOpen ( false ) ; } } style = { { textAlign : 'center' , padding : '5px 2px' , borderRadius : 4 , cursor : 'pointer' , background : isSel ? 'var(--primary)' : 'transparent' , color : isSel ? 'white' : isToday ? 'var(--primary)' : 'var(--text-primary)' , fontWeight : isToday ? 700 : 400 , position : 'relative' } } >
{ d }
{ eventDates . has ( key ) && ! isSel && < span style = { { position : 'absolute' , bottom : 2 , left : '50%' , transform : 'translateX(-50%)' , width : 4 , height : 4 , borderRadius : '50%' , background : 'var(--primary)' , display : 'block' } } / > }
< / div >
) ;
} ) }
< / div >
< / div >
) }
< / div >
) ;
}
// ── Event Type Popup ──────────────────────────────────────────────────────────
function EventTypePopup ( { userGroups , onSave , onClose , editing = null } ) {
const toast = useToast ( ) ;
@@ -61,34 +111,23 @@ function EventTypePopup({ userGroups, onSave, onClose, editing=null }) {
const [ colour , setColour ] = useState ( editing ? . colour || '#6366f1' ) ;
const [ groupId , setGroupId ] = useState ( editing ? . default _user _group _id || '' ) ;
const [ dur , setDur ] = useState ( editing ? . default _duration _hrs || 1 ) ;
const [ useDur , setUseDur ] = useState ( ! ! ( editing ? . default _duration _hrs && editing . default _duration _hrs !== 1 ) ) ;
const [ useDur , setUseDur ] = useState ( ! ! ( editing ? . default _duration _hrs ) ) ;
const [ saving , setSaving ] = useState ( false ) ;
const handle = async ( ) => {
if ( ! name . trim ( ) ) return toast ( 'Name required' , 'error' ) ;
setSaving ( true ) ;
try {
const body = { name : name . trim ( ) , colour , defaultUserGroupId : groupId || null , defaultDurationHrs : useDur ? dur : 1 } ;
const r = editing ? await api . updateEventType ( editing . id , body ) : await api . createEventType ( body ) ;
onSave ( r . eventType ) ; onClose ( ) ;
} catch ( e ) { toast ( e . message , 'error' ) ; } finally { setSaving ( false ) ; }
try {const body = { name : name . trim ( ) , colour , defaultUserGroupId : groupId || null , defaultDurationHrs : useDur ? dur : null } ; const r = editing ? await api . updateEventType ( editing . id , body ) : await api . createEventType ( body ) ; onSave ( r . eventType ) ; onClose ( ) ; } catch ( e ) { toast ( e . message , 'error' ) ; } finally { setSaving ( false ) ; }
} ;
return (
< div style = { { position : 'absolute' , top : '100%' , left : 0 , zIndex : 300 , background : 'var(--surface)' , border : '1px solid var(--border)' , borderRadius : 'var(--radius)' , padding : 16 , width : 270 , boxShadow : '0 4px 20px rgba(0,0,0,0.2)' } } >
< div style = { { marginBottom : 8 } } > < label className = "settings-section-label" > Name < / label > < input className = "input" value = { name } onChange = { e => setName ( e . target . value ) } style = { { marginTop : 4 } } autoFocus / > < / div >
< div style = { { marginBottom : 8 } } > < label className = "settings-section-label" > Colour < / label > < input type = "color" value = { colour } onChange = { e => setColour ( e . target . value ) } style = { { marginTop : 4 , width : '100%' , height : 32 , padding : 2 , borderRadius : 4 , border : '1px solid var(--border)' } } / > < / div >
< div style = { { marginBottom : 8 } } > < label className = "settings-section-label" > Default Group < / label >
< select className = "input" value = { groupId } onChange = { e => setGroupId ( e . target . value ) } style = { { marginTop : 4 } } >
< option value = "" > None < / option > { userGroups . map ( g => < option key = { g . id } value = { g . id } > { g . name } < / option > ) }
< / select >
< / div >
< div style = { { marginBottom : 8 } } > < label className = "settings-section-label" > Default Group < / label > < select className = "input" value = { groupId } onChange = { e => setGroupId ( e . target . value ) } style = { { marginTop : 4 } } > < option value = "" > None < / option > { userGroups . map ( g => < option key = { g . id } value = { g . id } > { g . name } < / option > ) } < / select > < / div >
< div style = { { marginBottom : 12 } } >
< label style = { { display : 'flex' , alignItems : 'center' , gap : 8 , fontSize : 13 , cursor : 'pointer' } } > < input type = "checkbox" checked = { useDur } onChange = { e => setUseDur ( e . target . checked ) } / > Set default duration < / label >
{ useDur && < select className = "input" value = { dur } onChange = { e => setDur ( Number ( e . target . value ) ) } style = { { marginTop : 6 } } > { DUR . map ( d => < option key = { d } value = { d } > { d } hr { d !== 1 ? 's' : '' } < / option > ) } < / select > }
< / div >
< div style = { { display : 'flex' , gap : 8 } } >
< button className = "btn btn-primary btn-sm" onClick = { handle } disabled = { saving } > { saving ? '…' : 'Save' } < / button >
< button className = "btn btn-secondary btn-sm" onClick = { onClose } > Cancel < / button >
< / div >
< div style = { { display : 'flex' , gap : 8 } } > < button className = "btn btn-primary btn-sm" onClick = { handle } disabled = { saving } > { saving ? '…' : 'Save' } < / button > < button className = "btn btn-secondary btn-sm" onClick = { onClose } > Cancel < / button > < / div >
< / div >
) ;
}
@@ -108,91 +147,141 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
const [ desc , setDesc ] = useState ( event ? . description || '' ) ;
const [ pub , setPub ] = useState ( event ? ! ! event . is _public : true ) ;
const [ track , setTrack ] = useState ( ! ! event ? . track _availability ) ;
const [ grou ps , setGrou ps ] = useState ( new Set ( ( event ? . user _groups || [ ] ) . map ( g => g . id ) ) ) ;
const [ grps , setGrps ] = useState ( new Set ( ( event ? . user _groups || [ ] ) . map ( g => g . id ) ) ) ;
const [ saving , setSaving ] = useState ( false ) ;
const [ showTypeForm , setShowTypeForm ] = useState ( false ) ;
const [ localTypes , setLocalTypes ] = useState ( eventTypes ) ;
const typeRef = useRef ( null ) ;
// Auto end time when type changes (only for new events)
useEffect ( ( ) => {
if ( ! typeId || event ) return ;
const typ = localTypes . find ( t => t . id === Number ( typeId ) ) ;
if ( ! typ || ! sd || ! st ) return ;
if ( ! typ ? . default _duration _hrs ||! sd || ! st ) return ;
const start = buildISO ( sd , st ) ;
setEd ( toDateIn ( addHours ( start , typ . default _duration _hrs ) ) ) ;
setEt ( toTimeIn ( addHours ( start , typ . default _duration _hrs ) ) ) ;
if ( typ . default _user _group _id ) setGrou ps ( prev => new Set ( [ ... prev , typ . default _user _group _id ] ) ) ;
if ( typ . default _user _group _id ) setGrps ( prev => new Set ( [ ... prev , Number ( typ . default _user _group _id ) ]) ) ;
} , [ typeId ] ) ;
const toggleGrp = id => setGroups ( prev => { const n = new Set ( prev ) ; n . has ( id ) ? n . delete ( id ) : n . add ( id ) ; return n ; } ) ;
// Auto-match end date to start date when start date changes
useEffect ( ( ) => {
if ( ! event ) setEd ( sd ) ;
} , [ sd ] ) ;
const toggleGrp = id => setGrps ( prev => { const n = new Set ( prev ) ; n . has ( id ) ? n . delete ( id ) : n . add ( id ) ; return n ; } ) ;
const groupsRequired = track ; // when tracking, groups are required
const handle = async ( ) => {
if ( ! title . trim ( ) ) return toast ( 'Title required' , 'error' ) ;
if ( ! allDay && ( ! sd || ! st || ! ed || ! et ) ) return toast ( 'Start and end required' , 'error' ) ;
if ( groupsRequired && grps . size === 0 ) return toast ( 'Select at least one group for availability tracking' , 'error' ) ;
setSaving ( true ) ;
try {
const body = { title : title . trim ( ) , eventTypeId : typeId || null , startAt : allDay ? ` ${ sd } T00:00:00 ` : buildISO ( sd , st ) , endAt : allDay ? ` ${ ed } T23:59:59 ` : buildISO ( ed , et ) , allDay , location : loc , description : desc , isPublic : pub , trackAvailability : track , userGroupIds : [ ... groups ] } ;
const r = event ? await api . updateEvent ( event . id , body ) : await api . createEvent ( body ) ;
onSave ( r . event ) ;
} catch ( e ) { toast ( e . message , 'error' ) ; } finally { setSaving ( false ) ; }
try {const body = { title : title . trim ( ) , eventTypeId : typeId || null , startAt : allDay ? ` ${ sd } T00:00:00 ` : buildISO ( sd , st ) , endAt : allDay ? ` ${ ed } T23:59:59 ` : buildISO ( ed , et ) , allDay , location : loc , description : desc , isPublic : pub , trackAvailability : track , userGroupIds : [ ... grps ] } ; const r = event ? await api . updateEvent ( event . id , body ) : await api . createEvent ( body ) ; onSave ( r . event ) ; } catch ( e ) { toast ( e . message , 'error' ) ; } finally { setSaving ( false ) ; }
} ;
const Row = ( { label , children } ) => (
< div style = { { display : 'flex' , alignItems : 'flex-start' , gap : 16 , marginBottom : 14 } } >
< div style = { { width : 9 0, flexShrink : 0 , fontSize : 13 , color : 'var(--text-tertiary)' , paddingTop : 8 , textAlign : 'right' } } > { label } < / div >
< div style = { { flex : 1 } } > { children } < / div >
const Row = ( { label , children , required }) => (
< div style = { { display : 'flex' , alignItems : 'flex-start' , gap : 0 , marginBottom : 16 } } >
< div style = { { width : 12 0, flexShrink : 0 , fontSize : 13 , color : 'var(--text-tertiary)' , paddingTop : 9 , paddingRight : 16 , textAlign : 'right' , whiteSpace : 'nowrap' } } >
{ label } { required && < span style = { { color : 'var(--error)' } } > * < / span > }
< / div >
< div style = { { flex : 1 , minWidth : 0 } } > { children } < / div >
< / div >
) ;
return (
< div style = { { display : 'flex ' , flexDirection : 'column' , maxWidth : 640 } } >
< div style = { { width : '100% ' , maxWidth : 1024 , overflowX : 'auto' } } >
< div style = { { minWidth : 500 } } >
{ /* Title */ }
< div style = { { marginBottom : 20 } } >
< input className = "input" placeholder = "Add title" value = { title } onChange = { e => setTitle ( e . target . value ) }
style = { { fontSize : 20 , fontWeight : 700 , marginBottom : 20 , border: 'none' , borderBottom : '2px solid var(--border)' , borderRadius : 0 , padding : '4px 0' , background : 'transparent' } } / >
< Row label = "" >
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 8 , alignItems : 'center' } } >
< input type = "date" className = "input" value = { sd } onChange = { e => setSd ( e . target . value ) } style = { { width : 150 } } / >
{ ! allDay && < > < input type = "time" className = "input" value = { st } onChange = { e => setSt ( e . target . value ) } style = { { width : 110 } } / > < span style = { { color : 'var(--text-tertiary)' , fontSize : 13 } } > to < / span > < input type = "time" className = "input" value = { et } onChange = { e => setEt ( e . target . value ) } style = { { width : 110 } } / > < / > }
< input type = "date" className = "input" value = { ed } onChange = { e => setEd ( e . target . value ) } style = { { width : 150 } } / >
style = { { fontSize : 20 , fontWeight : 700 , border : 'none' , borderBottom : '2px solid var(--border)' , borderRadius : 0 , padding : '4px 0' , background : 'transparent' , width : '100%' }} / >
< / div >
< label style = { { display : 'flex' , alignItems : 'center' , gap : 8 , marginTop : 8 , fontSize : 13 , cursor : 'pointer' } } >
< input type = "checkbox" checked = { allDay } onChange = { e => setAllDay ( e . target . checked ) } / > All day
{ /* Availability (first — if enabled, groups become required) */ }
< Row label = "Availability" >
< label style = { { display : 'flex' , alignItems : 'center' , gap : 10 , fontSize : 13 , cursor : 'pointer' , paddingTop : 6 } } >
< input type = "checkbox" checked = { track } onChange = { e => { setTrack ( e . target . checked ) ; if ( ! e . target . checked ) setPub ( true ) ; } } / >
Track availability for assigned groups
< / label >
< / Row >
< Row label = "Event Type" >
< div style = { { display : 'flex' , gap : 8 , alignItems : 'center' , position : 'relative' } } ref = { typeRef } >
< select className = "input flex-1" value = { typeId } onChange = { e => setTypeId ( e . target . value ) } >
< option value = "" > Default < / option >
{ localTypes . filter ( t => ! t . is _default ) . map ( t => < option key = { t . id } value = { t . id } > { t . name } < / option > ) }
< / select >
{ isToolManager && < button className = "btn btn-secondary btn-sm" onClick = { ( ) => setShowTypeForm ( v => ! v ) } > { showTypeForm ? 'Cancel' : '+ Type' } < / button > }
{ showTypeForm && < EventTypePopup userGroups = { userGroups } onSave = { et => { setLocalTypes ( p => [ ... p , et ] ) ; setShowTypeForm ( false ) ; } } onClose = { ( ) => setShowTypeForm ( false ) } / > }
< / div >
< / Row >
< Row label = "Groups" >
< div style = { { border : '1px solid var(--border)' , borderRadius : 'var(--radius)' , overflow : 'hidden' , maxHeight : 160 , overflowY : 'auto' } } >
{ userGroups . length === 0 ? < div style = { { padding : '10px 14px' , fontSize : 13 , color : 'var(--text-tertiary)' } } > No user groups yet < / div >
{ /* Groups — required when tracking */ }
< Row label = "Groups" requi red = { groupsRequired } >
< div >
< div style = { { border : ` 1px solid ${ groupsRequired && grps . size === 0 ? 'var(--error)' : 'var(--border)' } ` , borderRadius : 'var(--radius)' , overflow : 'hidden' , maxHeight : 160 , overflowY : 'auto' } } >
{ userGroups . length === 0
? < div style = { { padding : '10px 14px' , fontSize : 13 , color : 'var(--text-tertiary)' } } > No user groups yet < / div >
: userGroups . map ( g => (
< label key = { g . id } style = { { display : 'flex' , alignItems : 'center' , gap : 10 , padding : '7px 12px' , borderBottom : '1px solid var(--border)' , cursor : 'pointer' , fontSize : 13 } } >
< input type = "checkbox" checked = { grou ps . has ( g . id ) } onChange = { ( ) => toggleGrp ( g . id ) } style = { { accentColor : 'var(--primary)' } } / >
< input type = "checkbox" checked = { grps . has ( g . id ) } onChange = { ( ) => toggleGrp ( g . id ) } style = { { accentColor : 'var(--primary)' } } / >
{ g . name }
< / label >
) ) }
< / div >
< p style = { { fontSize : 11 , color : 'var(--text-tertiary)' , marginTop : 4 } } > { grou ps . size === 0 ? 'No groups — visible to all (if public)' : ` ${ groups . size } group ${ groups . size !== 1 ? 's' : '' } selected ` } < / p >
< / Row >
< Row label = "Options" >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
< label style = { { display : 'flex' , alignItems : 'center' , gap : 10 , fontSize : 13 , cursor : 'pointer' } } > < input type = "checkbox" checked = { ! pub } onChange = { e => setPub ( ! e . target . checked ) } / > Viewable by selected groups only < / label >
< label style = { { display : 'flex' , alignItems : 'center' , gap : 10 , fontSize : 13 , cursor : 'pointer' } } > < input type = "checkbox" checked = { track } onChange = { e => setTrack ( e . target . checked ) } / > Track availability for assigned groups < / label >
< p style = { { fontSize : 11 , color : groupsRequired && grps . size === 0 ? 'var(--error)' : 'var(--text-tertiary)' , marginTop : 4 } } >
{ grps . size === 0
? ( groupsRequired ? 'At least one group required for availability tracking' : 'No groups — event visible to all (if public)' )
: ` ${ grps . size } group ${ grps . size !== 1 ? 's' : '' } selected ` }
< / p >
< / div >
< / Row >
< Row label = "Location" > < input className = "input" placeholder = "Add location" value = { loc } onChange = { e => setLoc ( e . target . value ) } / > < / Row >
< Row label = "Description" > < textarea className = "input" placeholder = "Add description" value = { desc } onChange = { e => setDesc ( e . target . value ) } rows = { 3 } style = { { resize : 'vertical' } } / > < / Row >
{ /* Visibility — only shown if groups selected OR tracking */ }
{ ( grps . size > 0 || track ) && (
< Row label = "Visibility" >
< label style = { { display : 'flex' , alignItems : 'center' , gap : 10 , fontSize : 13 , cursor : 'pointer' , paddingTop : 6 } } >
< input type = "checkbox" checked = { ! pub } onChange = { e => setPub ( ! e . target . checked ) } / >
Viewable by selected groups only ( private )
< / label >
< / Row >
) }
{ /* Date/Time */ }
< Row label = "Date & Time" >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , flexWrap : 'nowrap' } } >
< input type = "date" className = "input" value = { sd } onChange = { e => setSd ( e . target . value ) } style = { { width : 150 , flexShrink : 0 } } / >
{ ! allDay && (
< >
< select className = "input" value = { st } onChange = { e => setSt ( e . target . value ) } style = { { width : 120 , flexShrink : 0 } } >
{ TIME _SLOTS . map ( s => < option key = { s . value } value = { s . value } > { s . label } < / option > ) }
< / select >
< span style = { { color : 'var(--text-tertiary)' , fontSize : 13 , flexShrink : 0 } } > to < / span >
< select className = "input" value = { et } onChange = { e => setEt ( e . target . value ) } style = { { width : 120 , flexShrink : 0 } } >
{ TIME _SLOTS . map ( s => < option key = { s . value } value = { s . value } > { s . label } < / option > ) }
< / select >
< input type = "date" className = "input" value = { ed } onChange = { e => setEd ( e . target . value ) } style = { { width : 150 , flexShrink : 0 } } / >
< / >
) }
< / div >
< label style = { { display : 'flex' , alignItems : 'center' , gap : 8 , fontSize : 13 , cursor : 'pointer' } } >
< input type = "checkbox" checked = { allDay } onChange = { e => setAllDay ( e . target . checked ) } / > All day
< / label >
< / div >
< / Row >
{ /* Event Type */ }
< Row label = "Event Type" >
< div style = { { display : 'flex' , gap : 8 , alignItems : 'center' , position : 'relative' } } ref = { typeRef } >
< select className = "input" value = { typeId } onChange = { e => setTypeId ( e . target . value ) } style = { { flex : 1 } } >
< option value = "" > Default < / option >
{ localTypes . filter ( t => ! t . is _default ) . map ( t => < option key = { t . id } value = { t . id } > { t . name } < / option > ) }
< / select >
{ isToolManager && < button className = "btn btn-secondary btn-sm" style = { { flexShrink : 0 } } onClick = { ( ) => setShowTypeForm ( v => ! v ) } > { showTypeForm ? 'Cancel' : '+ Type' } < / button > }
{ showTypeForm && < EventTypePopup userGroups = { userGroups } onSave = { et => { setLocalTypes ( p => [ ... p , et ] ) ; setShowTypeForm ( false ) ; } } onClose = { ( ) => setShowTypeForm ( false ) } / > }
< / div >
< / Row >
{ /* Location */ }
< Row label = "Location" >
< input className = "input" placeholder = "Add location" value = { loc } onChange = { e => setLoc ( e . target . value ) } / >
< / Row >
{ /* Description */ }
< Row label = "Description" >
< textarea className = "input" placeholder = "Add description" value = { desc } onChange = { e => setDesc ( e . target . value ) } rows = { 3 } style = { { resize : 'vertical' } } / >
< / Row >
< div style = { { display : 'flex' , gap : 8 , marginTop : 8 } } >
< button className = "btn btn-primary btn-sm" onClick = { handle } disabled = { saving } > { saving ? 'Saving…' : event ? 'Save Changes' : 'Create Event' } < / button >
@@ -200,30 +289,25 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
{ event && isToolManager && < button className = "btn btn-sm" style = { { marginLeft : 'auto' , background : 'var(--error)' , color : 'white' } } onClick = { ( ) => onDelete ( event ) } > Delete < / button > }
< / div >
< / div >
< / div >
) ;
}
// ── Event Detail Modal (portal) ───────────────────────────────────────────────
function EventDetailModal ( { event , onClose , onEdit , onAvailabilityChange , isToolManager , currentUserId } ) {
// ── Event Detail Modal ───────── ───────────────────────────────────────────────
function EventDetailModal ( { event , onClose , onEdit , onAvailabilityChange , isToolManager } ) {
const toast = useToast ( ) ;
const [ myResp , setMyResp ] = useState ( event . my _response ) ;
const [ avail , setAvail ] = useState ( event . availability || [ ] ) ;
const counts = { going : 0 , maybe : 0 , not _going : 0 } ;
avail . forEach ( r => { if ( counts [ r . response ] !== undefined ) counts [ r . response ] ++ ; } ) ;
const noRespCount = event . no _response _count || 0 ;
const handleResp = async resp => {
try {
if ( myResp === resp ) { await api . deleteAvailability ( event . id ) ; setMyResp ( null ) ; }
else { await api . setAvailability ( event . id , resp ) ; setMyResp ( resp ) ; }
onAvailabilityChange ? . ( ) ;
} catch ( e ) { toast ( e . message , 'error' ) ; }
try {if ( myResp === resp ) { await api . deleteAvailability ( event . id ) ; setMyResp ( null ) ; } else { await api . setAvailability ( event . id , resp ) ; setMyResp ( resp ) ; } onAvailabilityChange ? . ( ) ; } catch ( e ) { toast ( e . message , 'error' ) ; }
} ;
return ReactDOM . createPortal (
< div className = "modal-overlay" onClick = { e => e . target === e . currentTarget && onClose ( ) } >
< div className = "modal" style = { { maxWidth : 54 0 , maxHeight : '88vh' , overflowY : 'auto' } } >
{ /* Header */ }
< div className = "modal" style = { { maxWidth : 52 0 , maxHeight : '88vh' , overflowY : 'auto' } } >
< div style = { { display : 'flex' , alignItems : 'flex-start' , justifyContent : 'space-between' , marginBottom : 16 } } >
< div style = { { flex : 1 , paddingRight : 12 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 , marginBottom : 4 } } >
@@ -241,54 +325,30 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
< / div >
< / div >
{ /* Date/time */ }
< div style = { { display : 'flex' , gap : 10 , alignItems : 'center' , marginBottom : 12 , fontSize : 14 } } >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "var(--text-tertiary)" strokeWidth = "2" > < rect x = "3" y = "4" width = "18" height = "18" rx = "2" / > < line x1 = "16" y1 = "2" x2 = "16" y2 = "6" / > < line x1 = "8" y1 = "2" x2 = "8" y2 = "6" / > < line x1 = "3" y1 = "10" x2 = "21" y2 = "10" / > < / svg >
< span > { fmtDate ( new Date ( event . start _at ) ) } { ! event . all _day && ` · ${ fmtRange ( event . start _at , event . end _at ) } ` } < / span >
< / div >
{ event . location && < div style = { { display : 'flex' , gap : 10 , alignItems : 'center' , marginBottom : 12 , fontSize : 14 } } > < svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "var(--text-tertiary)" strokeWidth = "2" > < path d = "M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" / > < circle cx = "12" cy = "10" r = "3" / > < / svg > { event . location } < / div > }
{ event . description && < div style = { { display : 'flex' , gap : 10 , marginBottom : 12 , fontSize : 14 } } > < svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "var(--text-tertiary)" strokeWidth = "2" style = { { flexShrink : 0 , marginTop : 2 } } > < line x1 = "21" y1 = "10" x2 = "3" y2 = "10" / > < line x1 = "21" y1 = "6" x2 = "3" y2 = "6" / > < line x1 = "21" y1 = "14" x2 = "3" y2 = "14" / > < line x1 = "21" y1 = "18" x2 = "3" y2 = "18" / > < / svg > < span style = { { whiteSpace : 'pre-wrap' } } > { event . description } < / span > < / div > }
{ ( event . user _groups || [ ] ) . length > 0 && < div style = { { display : 'flex' , gap : 10 , marginBottom : 16 , fontSize : 13 , color : 'var(--text-secondary)' } } > < svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "var(--text-tertiary)" strokeWidth = "2" style = { { flexShrink : 0 , marginTop : 2 } } > < path d = "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < path d = "M23 21v-2a4 4 0 0 0-3-3.87" / > < / svg > { event . user _groups . map ( g => g . name ) . join ( ', ' ) } < / div > }
{ event . location && (
< div style = { { display : 'flex' , gap : 10 , alignItems : 'center' , marginBottom : 12 , fontSize : 14 } } >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "var(--text-tertiary)" strokeWidth = "2" > < path d = "M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" / > < circle cx = "12" cy = "10" r = "3" / > < / svg >
{ event . location }
< / div >
) }
{ event . description && (
< div style = { { display : 'flex' , gap : 10 , marginBottom : 12 , fontSize : 14 } } >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "var(--text-tertiary)" strokeWidth = "2" style = { { flexShrink : 0 , marginTop : 2 } } > < line x1 = "21" y1 = "10" x2 = "3" y2 = "10" / > < line x1 = "21" y1 = "6" x2 = "3" y2 = "6" / > < line x1 = "21" y1 = "14" x2 = "3" y2 = "14" / > < line x1 = "21" y1 = "18" x2 = "3" y2 = "18" / > < / svg >
< span style = { { whiteSpace : 'pre-wrap' } } > { event . description } < / span >
< / div >
) }
{ ( event . user _groups || [ ] ) . length > 0 && (
< div style = { { display : 'flex' , gap : 10 , marginBottom : 16 , fontSize : 13 , color : 'var(--text-secondary)' } } >
< svg width = "15" height = "15" viewBox = "0 0 24 24" fill = "none" stroke = "var(--text-tertiary)" strokeWidth = "2" style = { { flexShrink : 0 , marginTop : 2 } } > < path d = "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < path d = "M23 21v-2a4 4 0 0 0-3-3.87" / > < / svg >
{ event . user _groups . map ( g => g . name ) . join ( ', ' ) }
< / div >
) }
{ /* Availability section */ }
{ event . track _availability && (
< div style = { { borderTop : '1px solid var(--border)' , paddingTop : 16 , marginTop : 4 } } >
< div style = { { fontSize : 12 , fontWeight : 700 , color : 'var(--text-tertiary)' , textTransform : 'uppercase' , letterSpacing : '0.6px' , marginBottom : 10 } } > Your Availability < / div >
< div style = { { display : 'flex' , gap : 8 , marginBottom : 16 } } >
{ Object . entries ( RESP _LABEL ) . map ( ( [ key , label ] ) => (
< button key = { key } onClick = { ( ) => handleResp ( key ) } style = { { flex : 1 , padding : '8 px 4px' , borderRadius : 'var(--radius)' , border : ` 2px solid ${ RESP _COLOR [ key ] } ` , background : myResp === key ? RESP _COLOR [ key ] : 'transparent' , color : myResp === key ? 'white' : RESP _COLOR [ key ] , fontSize : 13 , fontWeight : 600 , cursor : 'pointer' , transition : 'all 0.15s' } } >
< button key = { key } onClick = { ( ) => handleResp ( key ) } style = { { flex : 1 , padding : '9 px 4px' , borderRadius : 'var(--radius)' , border : ` 2px solid ${ RESP _COLOR [ key ] } ` , background : myResp === key ? RESP _COLOR [ key ] : 'transparent' , color : myResp === key ? 'white' : RESP _COLOR [ key ] , fontSize : 13 , fontWeight : 600 , cursor : 'pointer' , transition : 'all 0.15s' } } >
{ myResp === key ? '✓ ' : '' } { label }
< / button >
) ) }
< / div >
{ /* Availability breakdown */ }
{ isToolManager && (
< >
< div style = { { fontSize : 12 , fontWeight : 700 , color : 'var(--text-tertiary)' , textTransform : 'uppercase' , letterSpacing : '0.6px' , marginBottom : 8 } } > Responses < / div >
< div style = { { display : 'flex' , gap : 20 , marginBottom : 10 , fontSize : 13 } } >
{ Object . entries ( counts ) . map ( ( [ key , n ] ) = >(
< span key = { key } > < span style = { { color : RESP _COLOR [ key ] , fontWeight : 700 } } > { n } < / span > { RESP _LABEL [ key ] } < / span >
) ) }
< span > < span style = { { fontWeight : 700 } } > { noRespCount } < / span > No response < / span >
{ Object . entries ( counts ) . map ( ( [ k , n ] )=> < span key = { k } > < span style = { { color : RESP _COLOR [ k ] , fontWeight : 700 } } > { n } < / span > { RESP _LABEL [ k ] } < / span > ) }
< span > < span style = { { fontWeight : 700 } } > { event . no _response _count || 0 } < / span > No response < / span >
< / div >
{ avail . length > 0 && (
< div style = { { border : '1px solid var(--border)' , borderRadius : 'var(--radius)' , overflow : 'hidden' } } >
@@ -311,7 +371,7 @@ function EventDetailModal({ event, onClose, onEdit, onAvailabilityChange, isTool
) ;
}
// ── Event Types Manager Panel ─────────────────────────────────────────────────
// ── Event Types Panel ──────── ─────────────────────────────────────────────────
function EventTypesPanel ( { eventTypes , userGroups , onUpdated } ) {
const toast = useToast ( ) ;
const [ editingType , setEditingType ] = useState ( null ) ;
@@ -324,24 +384,21 @@ function EventTypesPanel({ eventTypes, userGroups, onUpdated }) {
< div style = { { maxWidth : 560 } } >
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , marginBottom : 16 } } >
< div className = "settings-section-label" style = { { margin : 0 } } > Event Types < / div >
< div style = { { position : 'relative' } } >
< button className = "btn btn-primary btn-sm" onClick = { ( ) => { setShowForm ( v => ! v ) ; setEditingType ( null ) ; } } > + New Type < / button >
{ showForm && ! editingType && < EventTypePopup userGroups = { userGroups } onSave = { ( ) => onUpdated ( ) } onClose = { ( ) => setShowForm ( false ) } / > }
< / div >
< div style = { { position : 'relative' } } > < button className = "btn btn-primary btn-sm" onClick = { ( ) => { setShowForm ( v => ! v ) ; setEditingType ( null ) ; } } > + New Type < / button > { showForm && ! editingType && < EventTypePopup userGroups = { userGroups } onSave = { ( ) => onUpdated ( ) } onClose = { ( ) => setShowForm ( false ) } / > } < / div >
< / div >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 } } >
{ eventTypes . map ( et => (
< div key = { et . id } style = { { display : 'flex' , alignItems : 'center' , gap : 10 , padding : '9px 14px' , border : '1px solid var(--border)' , borderRadius : 'var(--radius)' } } >
< span style = { { width : 16 , height : 16 , borderRadius : '50%' , background : et . colour , flexShrink : 0 } } / >
< span style = { { flex : 1 , fontSize : 14 , fontWeight : 500 } } > { et . name } < / span >
{ et . default _duration _hrs > 1 &&< span style = { { fontSize : 12 , color : 'var(--text-tertiary)' } } > { et . default _duration _hrs } hr default < / span > }
{ ! et . is _default ? (
{ et . default _duration _hrs && < span style = { { fontSize : 12 , color : 'var(--text-tertiary)' } } > { et . default _duration _hrs } hr default < / span > }
{ ! et . is _protected ? (
< div style = { { display : 'flex' , gap : 6 , position : 'relative' } } >
< button className = "btn btn-secondary btn-sm" onClick = { ( ) => { setEditingType ( et ) ; setShowForm ( true ) ; } } > Edit < / button >
{ showForm && editingType ? . id === et . id && < EventTypePopup editing = { et } userGroups = { userGroups } onSave = { ( ) => { onUpdated ( ) ; setShowForm ( false ) ; setEditingType ( null ) ; } } onClose = { ( ) => { setShowForm ( false ) ; setEditingType ( null ) ; } } / > }
< button className = "btn btn-sm" style = { { background : 'var(--error)' , color : 'white' } } onClick = { ( ) => handleDel ( et ) } > Delete < / button >
< / div >
) : < span style = { { fontSize : 11 , color : 'var(--text-tertiary)' } } > Default < / span > }
) : < span style = { { fontSize : 11 , color : 'var(--text-tertiary)' } } > { et . is _default ? 'Default' : 'Protected' } < / span > }
< / div >
) ) }
< / div >
@@ -355,46 +412,14 @@ function BulkImportPanel({ onImported, onCancel }) {
const [ rows , setRows ] = useState ( null ) ;
const [ skipped , setSkipped ] = useState ( new Set ( ) ) ;
const [ saving , setSaving ] = useState ( false ) ;
const handleFile = async e => {
const file = e . target . files [ 0 ] ; if ( ! file ) return ;
try { const r = await api . importPreview ( file ) ; if ( r . error ) return toast ( r . error , 'error' ) ; setRows ( r . rows ) ; setSkipped ( new Set ( r . rows . filter ( r => r . duplicate || r . error ) . map ( r => r . row ) ) ) ; } catch { toast ( 'Upload failed' , 'error' ) ; }
} ;
const handleImport = async ( ) => {
setSaving ( true ) ;
try { const toImport = rows . filter ( r => ! skipped . has ( r . row ) && ! r . error ) ; const { imported } = await api . importConfirm ( toImport ) ; toast ( ` ${ imported } event ${ imported !== 1 ? 's' : '' } imported ` , 'success' ) ; onImported ( ) ; } catch ( e ) { toast ( e . message , 'error' ) ; } finally { setSaving ( false ) ; }
} ;
const handleFile = async e => { const file = e . target . files [ 0 ] ; if ( ! file ) return ; try { const r = await api . importPreview ( file ) ; if ( r . error ) return toast ( r . error , 'error' ) ; setRows ( r . rows ) ; setSkipped ( new Set ( r . rows . filter ( r => r . duplicate || r . error ) . map ( r => r . row ) ) ) ; } catch { toast ( 'Upload failed' , 'error' ) ; } } ;
const handleImport = async ( ) => { setSaving ( true ) ; try { const toImport = rows . filter ( r => ! skipped . has ( r . row ) && ! r . error ) ; const { imported } = await api . importConfirm ( toImport ) ; toast ( ` ${ imported } event ${ imported !== 1 ? 's' : '' } imported ` , 'success' ) ; onImported ( ) ; } catch ( e ) { toast ( e . message , 'error' ) ; } finally { setSaving ( false ) ; } } ;
return (
< div style = { { maxWidth : 800 } } >
< div className = "settings-section-label" > Bulk Event Import < / div >
< p style = { { fontSize : 12 , color : 'var(--text-tertiary)' , marginBottom : 12 } } > CSV : < code > Event Title , start _date ( YYYY - MM - DD ) , start _time ( HH : MM ) , event _location , event _type , default _duration < / code > < / p >
< input type = "file" accept = ".csv" onChange = { handleFile } style = { { marginBottom : 16 } } / >
{ rows && (
< >
< div style = { { overflowX : 'auto' , marginBottom : 12 } } >
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 12 } } >
< thead > < tr style = { { borderBottom : '2px solid var(--border)' } } >
{ [ '' , 'Row' , 'Title' , 'Start' , 'End' , 'Type' , 'Dur' , 'Status' ] . map ( h => < th key = { h } style = { { padding : '4px 8px' , textAlign : 'left' , color : 'var(--text-tertiary)' , whiteSpace : 'nowrap' } } > { h } < / th > ) }
< / tr > < / thead >
< tbody > { rows . map ( r => (
< tr key = { r . row } style = { { borderBottom : '1px solid var(--border)' , opacity : skipped . has ( r . row ) ? 0.45 : 1 } } >
< td style = { { padding : '4px 8px' } } > < input type = "checkbox" checked = { ! skipped . has ( r . row ) } disabled = { ! ! r . error } onChange = { ( ) => setSkipped ( p => { const n = new Set ( p ) ; n . has ( r . row ) ? n . delete ( r . row ) : n . add ( r . row ) ; return n ; } ) } / > < / td >
< td style = { { padding : '4px 8px' } } > { r . row } < / td >
< td style = { { padding : '4px 8px' , fontWeight : 600 } } > { r . title } < / td >
< td style = { { padding : '4px 8px' } } > { r . startAt ? . slice ( 0 , 16 ) . replace ( 'T' , ' ' ) } < / td >
< td style = { { padding : '4px 8px' } } > { r . endAt ? . slice ( 0 , 16 ) . replace ( 'T' , ' ' ) } < / td >
< td style = { { padding : '4px 8px' } } > { r . typeName } < / td >
< td style = { { padding : '4px 8px' } } > { r . durHrs } hr < / td >
< td style = { { padding : '4px 8px' } } > { r . error ? < span style = { { color : 'var(--error)' } } > { r . error } < / span > : r . duplicate ? < span style = { { color : '#f59e0b' } } > ⚠ Duplicate < / span > : < span style = { { color : 'var(--success)' } } > ✓ Ready < / span > } < / td >
< / tr >
) ) } < / tbody >
< / table >
< / div >
< div style = { { display : 'flex' , gap : 8 , alignItems : 'center' } } >
< button className = "btn btn-primary btn-sm" onClick = { handleImport } disabled = { saving } > { saving ? 'Importing…' : ` Import ${ rows . filter ( r => ! skipped . has ( r . row ) && ! r . error ) . length } events ` } < / button >
< button className = "btn btn-secondary btn-sm" onClick = { onCancel } > Cancel < / button >
< / div >
< / >
) }
{ rows && ( < > < div style = { { overflowX : 'auto' , marginBottom : 12 } } > < table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : 12 } } > < thead > < tr style = { { borderBottom : '2px solid var(--border)' } } > { [ '' , 'Row' , 'Title' , 'Start' , 'End' , 'Type' , 'Dur' , 'Status' ] . map ( h => < th key = { h } style = { { padding : '4px 8px' , textAlign : 'left' , color : 'var(--text-tertiary)' , whiteSpace : 'nowrap' } } > { h } < / th > ) } < / tr > < / thead > < tbody > { rows . map ( r => ( < tr key = { r . row } style = { { borderBottom : '1px solid var(--border)' , opacity : skipped . has ( r . row ) ? 0.45 : 1 } } > < td style = { { padding : '4px 8px' } } > < input type = "checkbox" checked = { ! skipped . has ( r . row ) } disabled = { ! ! r . error } onChange = { ( ) => setSkipped ( p => { const n = new Set ( p ) ; n . has ( r . row ) ? n . delete ( r . row ) : n . add ( r . row ) ; return n ; } ) } / > < / td > < td style = { { padding : '4px 8px' } } > { r . row } < / td > < td style = { { padding : '4px 8px' , fontWeight : 600 } } > { r . title } < / td > < td style = { { padding : '4px 8px' } } > { r . startAt ? . slice ( 0 , 16 ) . replace ( 'T' , ' ' ) } < / td > < td style = { { padding : '4px 8px' } } > { r . endAt ? . slice ( 0 , 16 ) . replace ( 'T' , ' ' ) } < / td > < td style = { { padding : '4px 8px' } } > { r . typeName } < / td > < td style = { { padding : '4px 8px' } } > { r . durHrs } hr < / td > < td style = { { padding : '4px 8px' } } > { r . error ? < span style = { { color : 'var(--error)' } } > { r . error } < / span > : r . duplicate ? < span style = { { color : '#f59e0b' } } > ⚠ Duplicate < / span > : < span style = { { color : 'var(--success)' } } > ✓ Ready < / span > } < / td > < / tr > ) ) } < / tbody > < / table > < / div > < div style = { { display : 'flex' , gap : 8 } } > < button className = "btn btn-primary btn-sm" onClick = { handleImport } disabled = { saving } > { saving ? 'Importing…' : ` Import ${ rows . filter ( r => ! skipped . has ( r . row ) && ! r . error ) . length } events ` } < / button > < button className = "btn btn-secondary btn-sm" onClick = { onCancel } > Cancel < / button > < / div > < / > ) }
< / div >
) ;
}
@@ -402,131 +427,43 @@ function BulkImportPanel({ onImported, onCancel }) {
// ── Calendar Views ────────────────────────────────────────────────────────────
function ScheduleView ( { events , selectedDate , onSelect } ) {
const filtered = events . filter ( e => new Date ( e . end _at ) >= ( selectedDate || new Date ( 0 ) ) ) ;
if ( ! filtered . length ) return < div style = { { textAlign : 'center' , padding : '60px 20px' , color : 'var(--text-tertiary)' , fontSize : 14 } } > No upcoming events < / div > ;
return filtered . map ( e => {
const s = new Date ( e . start _at ) ; const col = e . event _type ? . colour || '#9ca3af' ;
return (
< div key = { e . id } onClick = { ( ) => onSelect ( e ) } style = { { display : 'flex' , alignItems : 'center' , gap : 20 , padding : '14px 20px' , borderBottom : '1px solid var(--border)' , cursor : 'pointer' } }
onMouseEnter = { el => el . currentTarget . style . background = 'var(--background)' } onMouseLeave = { el => el . currentTarget . style . background = '' } >
< div style = { { width : 44 , textAlign : 'center' , flexShrink : 0 } } >
< div style = { { fontSize : 22 , fontWeight : 700 , lineHeight : 1 } } > { s . getDate ( ) } < / div >
< div style = { { fontSize : 11 , color : 'var(--text-tertiary)' , textTransform : 'uppercase' } } > { SHORT _MONTHS [ s . getMonth ( ) ] } , { DAYS [ s . getDay ( ) ] } < / div >
< / div >
< div style = { { width : 100 , flexShrink : 0 , display : 'flex' , alignItems : 'center' , gap : 8 , fontSize : 13 , color : 'var(--text-secondary)' } } >
< span style = { { width : 10 , height : 10 , borderRadius : '50%' , background : col , flexShrink : 0 } } / >
{ e . all _day ? 'All day' : fmtRange ( e . start _at , e . end _at ) }
< / div >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { fontSize : 14 , fontWeight : 600 , display : 'flex' , alignItems : 'center' , gap : 8 } } >
{ e . event _type ? . name && < span style = { { fontSize : 11 , color : 'var(--text-tertiary)' , textTransform : 'uppercase' , letterSpacing : '0.5px' , fontWeight : 600 } } > { e . event _type . name } : < / span > }
{ e . title }
{ e . track _availability && ! e . my _response && < span style = { { width : 8 , height : 8 , borderRadius : '50%' , background : '#ef4444' , flexShrink : 0 } } title = "Awaiting your response" / > }
< / div >
{ e . location && < div style = { { fontSize : 12 , color : 'var(--text-tertiary)' , marginTop : 2 } } > { e . location } < / div > }
< / div >
< / div >
) ;
} ) ;
if ( ! filtered . length ) return < div style = { { textAlign : 'center' , padding : '60px 20px' , color : 'var(--text-tertiary)' , fontSize : 14 } } > No upcoming events from selected date < / div > ;
return < > { filtered . map ( e => { const s = new Date ( e . start _at ) ; const col = e . event _type ? . colour || '#9ca3af' ; return ( < div key = { e . id } onClick = { ( ) => onSelect ( e ) } style = { { display : 'flex' , alignItems : 'center' , gap : 20 , padding : '14px 20px' , borderBottom : '1px solid var(--border)' , cursor : 'pointer' } } onMouseEnter = { el => el . currentTarget . style . background = 'var(--background)' } onMouseLeave = { el => el . currentTarget . style . background = '' } > < div style = { { width : 44 , textAlign : 'center' , flexShrink : 0 } } > < div style = { { fontSize : 22 , fontWeight : 700 , lineHeight : 1 } } > { s . getDate ( ) } < / div > < div style = { { fontSize : 11 , color : 'var(--text-tertiary)' , textTransform : 'uppercase' } } > { SHORT _MONTHS [ s . getMonth ( ) ] } , { DAYS [ s . getDay ( ) ] } < / div > < / div > < div style = { { width : 100 , flexShrink : 0 , display : 'flex' , alignItems : 'center' , gap : 8 , fontSize : 13 , color : 'var(--text-secondary)' } } > < span style = { { width : 10 , height : 10 , borderRadius : '50%' , background : col , flexShrink : 0 } } / > { e . all _day ? 'All day' : fmtRange ( e . start _at , e . end _at ) } < / div > < div style = { { flex : 1 , minWidth : 0 } } > < div style = { { fontSize : 14 , fontWeight : 600 , display : 'flex' , alignItems : 'center' , gap : 8 } } > { e . event _type ? . name && < span style = { { fontSize : 11 , color : 'var(--text-tertiary)' , textTransform : 'uppercase' , letterSpacing : '0.5px' , fontWeight : 600 } } > { e . event _type . name } : < / span > } { e . title } { e . track _availability && ! e . my _response && < span style = { { width : 8 , height : 8 , borderRadius : '50%' , background : '#ef4444' , flexShrink : 0 } } title = "Awaiting your response" / > } < / div > { e . location && < div style = { { fontSize : 12 , color : 'var(--text-tertiary)' , marginTop : 2 } } > { e . location } < / div > } < / div > < / div > ) ; } ) } < / > ;
}
function DayView ( { events , selectedDate , onSelect } ) {
const hours = Array . from ( { length : 16 } , ( _ , i ) => i + 7 ) ;
const day = events . filter ( e => sameDay ( new Date ( e . start _at ) , selectedDate ) ) ;
return (
< div >
< div style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , padding : '8px 0 8px 60px' , fontSize : 13 , fontWeight : 600 , color : 'var(--primary)' } } >
< div style = { { textAlign : 'center' } } > < div > { DAYS [ selectedDate . getDay ( ) ] } < / div > < div style = { { fontSize : 28 , fontWeight : 700 } } > { selectedDate . getDate ( ) } < / div > < / div >
< / div >
{ hours . map ( h => (
< div key = { h } style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , minHeight : 52 } } >
< div style = { { width : 60 , flexShrink : 0 , fontSize : 11 , color : 'var(--text-tertiary)' , padding : '3px 10px 0' , textAlign : 'right' } } >
{ h > 12 ? ` ${ h - 12 } PM ` : h === 12 ? '12 PM' : ` ${ h } AM ` }
< / div >
< div style = { { flex : 1 , padding : '2px 4px' } } >
{ day . filter ( e => new Date ( e . start _at ) . getHours ( ) === h ) . map ( e => (
< div key = { e . id } onClick = { ( ) => onSelect ( e ) } style = { { margin : '2px 0' , padding : '5px 10px' , borderRadius : 5 , background : e . event _type ? . colour || '#6366f1' , color : 'white' , fontSize : 12 , cursor : 'pointer' , fontWeight : 600 } } >
{ e . title } · { fmtRange ( e . start _at , e . end _at ) }
< / div >
) ) }
< / div >
< / div >
) ) }
< / div >
) ;
return (< div > < div style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , padding : '8px 0 8px 60px' , fontSize : 13 , fontWeight : 600 , color : 'var(--primary)' } } > < div style = { { textAlign : 'center' } } > < div > { DAYS [ selectedDate . getDay ( ) ] } < / div > < div style = { { fontSize : 28 , fontWeight : 700 } } > { selectedDate . getDate ( ) } < / div > < / div > < / div > { hours . map ( h => ( < div key = { h } style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , minHeight : 52 } } > < div style = { { width : 60 , flexShrink : 0 , fontSize : 11 , color : 'var(--text-tertiary)' , padding : '3px 10px 0' , textAlign : 'right' } } > { h > 12 ? ` ${ h - 12 } PM ` : h === 12 ? '12 PM' : ` ${ h } AM ` } < / div > < div style = { { flex : 1 , padding : '2px 4px' } } > { day . filter ( e => new Date ( e . start _at ) . getHours ( ) === h ) . map ( e => ( < div key = { e . id } onClick = { ( ) => onSelect ( e ) } style = { { margin : '2px 0' , padding : '5px 10px' , borderRadius : 5 , background : e . event _type ? . colour || '#6366f1' , color : 'white' , fontSize : 12 , cursor : 'pointer' , fontWeight : 600 } } > { e . title } · { fmtRange ( e . start _at , e . end _at ) } < / div > ) ) } < / div > < / div > ) ) } < / div > ) ;
}
function WeekView ( { events , selectedDate , onSelect } ) {
const ws = weekStart ( selectedDate ) , days = Array . from ( { length : 7 } , ( _ , i ) => { const d = new Date ( ws ) ; d . setDate ( d . getDate ( ) + i ) ; return d ; } ) ;
const hours = Array . from ( { length : 16 } , ( _ , i ) => i + 7 ) , today = new Date ( ) ;
return (
< div >
< div style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , borderBottom : '1px solid var(--border)' } } >
< div / >
{ days . map ( ( d , i ) => < div key = { i } style = { { textAlign : 'center' , padding : '6px 4px' , fontSize : 12 , fontWeight : 600 , color : sameDay ( d , today ) ? 'var(--primary)' : 'var(--text-secondary)' } } > { DAYS [ d . getDay ( ) ] } { d . getDate ( ) } < / div > ) }
< / div >
{ hours . map ( h => (
< div key = { h } style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , borderBottom : '1px solid var(--border)' , minHeight : 46 } } >
< div style = { { fontSize : 11 , color : 'var(--text-tertiary)' , padding : '3px 10px 0' , textAlign : 'right' } } > { h > 12 ? ` ${ h - 12 } PM ` : h === 12 ? '12 PM' : ` ${ h } AM ` } < / div >
{ days . map ( ( d , i ) => (
< div key = { i } style = { { borderLeft : '1px solid var(--border)' , padding : '1px 2px' } } >
{ events . filter ( e => sameDay ( new Date ( e . start _at ) , d ) && new Date ( e . start _at ) . getHours ( ) === h ) . map ( e => (
< div key = { e . id } onClick = { ( ) => onSelect ( e ) } style = { { background : e . event _type ? . colour || '#6366f1' , color : 'white' , borderRadius : 3 , padding : '2px 5px' , fontSize : 11 , cursor : 'pointer' , marginBottom : 1 , fontWeight : 600 , overflow : 'hidden' , whiteSpace : 'nowrap' , textOverflow : 'ellipsis' } } >
{ e . title }
< / div >
) ) }
< / div >
) ) }
< / div >
) ) }
< / div >
) ;
return (< div > < div style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , borderBottom : '1px solid var(--border)' } } > < div / > { days . map ( ( d , i ) => < div key = { i } style = { { textAlign : 'center' , padding : '6px 4px' , fontSize : 12 , fontWeight : 600 , color : sameDay ( d , today ) ? 'var(--primary)' : 'var(--text-secondary)' } } > { DAYS [ d . getDay ( ) ] } { d . getDate ( ) } < / div > ) } < / div > { hours . map ( h => ( < div key = { h } style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , borderBottom : '1px solid var(--border)' , minHeight : 46 } } > < div style = { { fontSize : 11 , color : 'var(--text-tertiary)' , padding : '3px 10px 0' , textAlign : 'right' } } > { h > 12 ? ` ${ h - 12 } PM ` : h === 12 ? '12 PM' : ` ${ h } AM ` } < / div > { days . map ( ( d , i ) => ( < div key = { i } style = { { borderLeft : '1px solid var(--border)' , padding : '1px 2px' } } > { events . filter ( e => sameDay ( new Date ( e . start _at ) , d ) && new Date ( e . start _at ) . getHours ( ) === h ) . map ( e => ( < div key = { e . id } onClick = { ( ) => onSelect ( e ) } style = { { background : e . event _type ? . colour || '#6366f1' , color : 'white' , borderRadius : 3 , padding : '2px 5px' , fontSize : 11 , cursor : 'pointer' , marginBottom : 1 , fontWeight : 600 , overflow : 'hidden' , whiteSpace : 'nowrap' , textOverflow : 'ellipsis' } } > { e . title } < / div > ) ) } < / div > ) ) } < / div > ) ) } < / div > ) ;
}
function MonthView ( { events , selectedDate , onSelect , onSelectDay } ) {
const y = selectedDate . getFullYear ( ) , m = selectedDate . getMonth ( ) ;
const first = new Date ( y , m , 1 ) . getDay ( ) , total = daysInMonth ( y , m ) , today = new Date ( ) ;
const y = selectedDate . getFullYear ( ) , m = selectedDate . getMonth ( ) , first = new Date ( y , m , 1 ) . getDay ( ) , total = daysInMonth ( y , m ) , today = new Date ( ) ;
const cells = [ ] ; for ( let i = 0 ; i < first ; i + + ) cells.push ( null ) ; for ( let d = 1 ; d < = total ; d + + ) cells.push ( d ) ;
while ( cells.length % 7 ! = = 0 ) cells.push ( null ) ;
const weeks = [ ] ; for ( let i = 0 ; i < cells.length ; i + = 7 ) weeks.push ( cells.slice ( i , i + 7 ) ) ;
return (
< div >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(7,1fr)' , borderBottom : '1px solid var(--border)' } } >
{ DAYS . map ( d => < div key = { d } style = { { textAlign : 'center' , padding : '8px' , fontSize : 12 , fontWeight : 600 , color : 'var(--text-tertiary)' } } > { d } < / div > ) }
< / div >
{ weeks . map ( ( week , wi ) => (
< div key = { wi } style = { { display : 'grid' , gridTemplateColumns : 'repeat(7,1fr)' } } >
{ week . map ( ( d , di ) => {
if ( ! d ) return < div key = { di } style = { { borderRight : '1px solid var(--border)' , borderBottom : '1px solid var(--border)' , minHeight : 90 , background : 'var(--surface-variant)' } } / > ;
const date = new Date ( y , m , d ) , dayEvs = events . filter ( e => sameDay ( new Date ( e . start _at ) , date ) ) , isToday = sameDay ( date , today ) ;
return (
< div key = { di } onClick = { ( ) => onSelectDay ( date ) } style = { { borderRight : '1px solid var(--border)' , borderBottom : '1px solid var(--border)' , padding : '4px' , minHeight : 90 , cursor : 'pointer' } }
onMouseEnter = { el => el . currentTarget . style . background = 'var(--background)' } onMouseLeave = { el => el . currentTarget . style . background = '' } >
< div style = { { width : 26 , height : 26 , borderRadius : '50%' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , marginBottom : 3 , fontSize : 13 , fontWeight : isToday ? 700 : 400 , background : isToday ? 'var(--primary)' : 'transparent' , color : isToday ? 'white' : 'var(--text-primary)' } } > { d } < / div >
{ dayEvs . slice ( 0 , 3 ) . map ( e => (
< div key = { e . id } onClick = { ev => { ev . stopPropagation ( ) ; onSelect ( e ) ; } } style = { { background : e . event _type ? . colour || '#6366f1' , color : 'white' , borderRadius : 3 , padding : '1px 5px' , fontSize : 11 , marginBottom : 2 , cursor : 'pointer' , whiteSpace : 'nowrap' , overflow : 'hidden' , textOverflow : 'ellipsis' } } >
{ ! e . all _day && < span style = { { marginRight : 3 } } > { fmtTime ( e . start _at ) } < / span > } { e . title }
< / div >
) ) }
{ dayEvs . length > 3 && < div style = { { fontSize : 10 , color : 'var(--text-tertiary)' } } > + { dayEvs . length - 3 } more < / div > }
< / div >
) ;
} ) }
< / div >
) ) }
< / div >
) ;
return (< div > < div style = { { display : 'grid' , gridTemplateColumns : 'repeat(7,1fr)' , borderBottom : '1px solid var(--border)' } } > { DAYS . map ( d => < div key = { d } style = { { textAlign : 'center' , padding : '8px' , fontSize : 12 , fontWeight : 600 , color : 'var(--text-tertiary)' } } > { d } < / div > ) } < / div > { weeks . map ( ( week , wi ) => ( < div key = { wi } style = { { display : 'grid' , gridTemplateColumns : 'repeat(7,1fr)' } } > { week . map ( ( d , di ) => { if ( ! d ) return < div key = { di } style = { { borderRight : '1px solid var(--border)' , borderBottom : '1px solid var(--border)' , minHeight : 90 , background : 'var(--surface-variant)' } } / > ; const date = new Date ( y , m , d ) , dayEvs = events . filter ( e => sameDay ( new Date ( e . start _at ) , date ) ) , isToday = sameDay ( date , today ) ; return ( < div key = { di } onClick = { ( ) => onSelectDay ( date ) } style = { { borderRight : '1px solid var(--border)' , borderBottom : '1px solid var(--border)' , padding : '4px' , minHeight : 90 , cursor : 'pointer' } } onMouseEnter = { el => el . currentTarget . style . background = 'var(--background)' } onMouseLeave = { el => el . currentTarget . style . background = '' } > < div style = { { width : 26 , height : 26 , borderRadius : '50%' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , marginBottom : 3 , fontSize : 13 , fontWeight : isToday ? 700 : 400 , background : isToday ? 'var(--primary)' : 'transparent' , color : isToday ? 'white' : 'var(--text-primary)' } } > { d } < / div > { dayEvs . slice ( 0 , 3 ) . map ( e => ( < div key = { e . id } onClick = { ev => { ev . stopPropagation ( ) ; onSelect ( e ) ; } } style = { { background : e . event _type ? . colour || '#6366f1' , color : 'white' , borderRadius : 3 , padding : '1px 5px' , fontSize : 11 , marginBottom : 2 , cursor : 'pointer' , whiteSpace : 'nowrap' , overflow : 'hidden' , textOverflow : 'ellipsis' } } > { ! e . all _day && < span style = { { marginRight : 3 } } > { fmtTime ( e . start _at ) } < / span > } { e . title } < / div > ) ) } { dayEvs . length > 3 && < div style = { { fontSize : 10 , color : 'var(--text-tertiary)' } } > + { dayEvs . length - 3 } more < / div > } < / div > ) ; } ) } < / div > ) ) } < / div > ) ;
}
// ── Main Schedule Page ────────────────────────────────────────────────────────
export default function SchedulePage ( { onBack , isToolManager } ) {
export default function SchedulePage ( { isToolManager, isMobile } ) {
const { user } = useAuth ( ) ;
const toast = useToast ( ) ;
// Mobile: only day + schedule views
const allowedViews = isMobile ? [ 'schedule' , 'day' ] : [ 'schedule' , 'day' , 'week' , 'month' ] ;
const [ view , setView ] = useState ( 'schedule' ) ;
const [ selDate , setSelDate ] = useState ( new Date ( ) ) ;
const [ events , setEvents ] = useState ( [ ] ) ;
const [ eventTypes , setEventTypes ] = useState ( [ ] ) ;
const [ userGroups , setUserGroups ] = useState ( [ ] ) ;
const [ panel , setPanel ] = useState ( 'calendar' ) ; // calendar | eventForm | eventTypes | bulkImport
const [ panel , setPanel ] = useState ( 'calendar' ) ;
const [ editingEvent , setEditingEvent ] = useState ( null ) ;
const [ detailEvent , setDetailEvent ] = useState ( null ) ;
const [ loading , setLoading ] = useState ( true ) ;
@@ -574,28 +511,31 @@ export default function SchedulePage({ onBack, isToolManager }) {
try { await api . deleteEvent ( e . id ) ; toast ( 'Deleted' , 'success' ) ; load ( ) ; setDetailEvent ( null ) ; } catch ( err ) { toast ( err . message , 'error' ) ; }
} ;
if ( loading ) return (
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' , height : '100vh' , color : 'var(--text-tertiary)' , fontSize : 14 } } > Loading schedule … < / div >
) ;
if ( loading ) return < div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' , flex : 1 , color : 'var(--text-tertiary)' , fontSize : 14 } } > Loading schedule … < / div > ;
// ── Sidebar width matches Messages (~280px) ───────────────────────────────
const SIDEBAR _W = isMobile ? 0 : 260 ;
return (
< div style = { { display : 'flex' , flexDirection : 'column' , height : '100vh' , background : 'var(--background)' } } >
{ /* Top bar */ }
< div style = { { display : 'flex' , alignItems : 'center' , gap : 12 , padding : '10px 20px' , borderBottom : '1px solid var(--border)' , background : 'var(--surface)' , flexShrink : 0 , flexWrap : 'wrap' } } >
{ /* Back to Messages */ }
< button className = "btn btn-secondary btn-sm" onClick = { onBack } style = { { display : 'flex' , alignItems : 'center' , gap : 6 } } >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < polyline points = "15 18 9 12 15 6" / > < / svg >
Messages
< / button >
< div style = { { display : 'flex' , flex: 1 , overflow : 'hidden' , minHeight : 0 } } >
{ /* Left panel — matches sidebar width */ }
{ ! isMobile && (
< div style = { { width : SIDEBAR _W , flexShrink : 0 , borderRight : '1px solid var(--border)' , display : 'flex' , flexDirection : 'column' , background : 'var(--surface)' , overflow : 'hidden' } } >
< div style = { { padding : '16px 16px 8px' , borderBottom : '1px solid var(--border)' } } >
< div style = { { fontSize : 16 , fontWeight : 700 , marginBottom : 12 , color : 'var(--text-primary)' } } > Team Schedule < / div >
{ /* Create dropdow n */ }
{ /* Create button — styled like new-chat-bt n */ }
{ isToolManager && (
< div style = { { position : 'relative' } } ref = { createRef } >
< button className = "btn btn-primary btn-sm " onClick = { ( ) => setCreateOpen ( v => ! v ) } style = { { display : 'flex ' , alignItems : 'center' , gap : 6 } } >
+ Create < svg width = "12" height = "12 " viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeW idth= "2" > < polyline points = "6 9 12 15 18 9" / > < / svg >
< div style = { { position : 'relative' , marginBottom : 12 } } ref = { createRef } >
< button className = "newchat-btn " onClick = { ( ) => setCreateOpen ( v => ! v ) } style = { { width : '100% ' , justifyContent : 'center' , gap : 8 } } >
< svg xmlns = "http://www.w3.org/2000/svg" fill = "none " viewBox = "0 0 24 24" strokeWidth = { 1.5 } stroke = "currentColor" w idth= "18" height = "18" >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M12 4.5v15m7.5-7.5h-15" / >
< / svg >
Create Event
< svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < polyline points = "6 9 12 15 18 9" / > < / svg >
< / button >
{ createOpen && (
< div style = { { position : 'absolute' , top : '100%' , left : 0 , zIndex : 100 , background : 'var(--surface)' , border : '1px solid var(--border)' , borderRadius : 'var(--radius)' , marginTop : 4 , minWidth : 180 , boxShadow : '0 4px 16px rgba(0,0,0,0.12)' } } >
< div style = { { position : 'absolute' , top : '100%' , left : 0 , right : 0 , zIndex : 100 , background : 'var(--surface)' , border : '1px solid var(--border)' , borderRadius : 'var(--radius)' , marginTop : 4 , boxShadow : '0 4px 16px rgba(0,0,0,0.12)' } } >
{ [ [ 'Event' , ( ) => { setPanel ( 'eventForm' ) ; setEditingEvent ( null ) ; setCreateOpen ( false ) ; } ] ,
[ 'Event Type' , ( ) => { setPanel ( 'eventTypes' ) ; setCreateOpen ( false ) ; } ] ,
[ 'Bulk Event Import' , ( ) => { setPanel ( 'bulkImport' ) ; setCreateOpen ( false ) ; } ]
@@ -607,56 +547,81 @@ export default function SchedulePage({ onBack, isToolManager }) {
) }
< / div >
) }
{ /* Navigation */ }
< button className = "btn btn-secondary btn-sm" onClick = { ( ) => setSelDate ( new Date ( ) ) } > Today < / button >
< div style = { { display : 'flex' , gap : 2 } } >
< button className = "btn-icon" onClick = { ( ) => navDate ( - 1 ) } > ‹ < / button >
< button className = "btn-icon" onClick = { ( ) => navDate ( 1 ) } > › < / button >
< / div >
{ view !== 'schedule' && < span style = { { fontSize : 14 , fontWeight : 600 , color : 'var(--text-primary)' } } > { navLabel ( ) } < / span > }
< div style = { { marginLeft : 'auto' , display : 'flex' , gap : 2 , background : 'var(--surface-variant)' , borderRadius : 'var(--radius)' , padding : 3 } } >
{ [ [ 'schedule' , 'Schedule' ] , [ 'day' , 'Day' ] , [ 'week' , 'Week' ] , [ 'month' , 'Month' ] ] . map ( ( [ v , l ] ) => (
< button key = { v } onClick = { ( ) => { setView ( v ) ; setPanel ( 'calendar' ) ; } } style = { { padding : '4px 12px' , borderRadius : 5 , border : 'none' , cursor : 'pointer' , fontSize : 12 , fontWeight : 600 , background : view === v ? 'var(--surface)' : 'transparent' , color : view === v ? 'var(--text-primary)' : 'var(--text-tertiary)' , boxShadow : view === v ? '0 1px 3px rgba(0,0,0,0.1)' : 'none' , transition : 'all 0.15s' } } > { l } < / button >
) ) }
< / div >
< / div >
{ /* Body: left m ini- cal + right content */ }
< div style = { { display : 'flex' , flex : 1 , overflow : 'hidden' } } >
{ /* Left: mini calendar */ }
< div style = { { width : 220 , flexShrink : 0 , borderRight : '1px solid var(--border)' , padding : 16 , background : 'var(--surface)' , overflowY : 'auto' } } >
{ /* M ini calendar */ }
< div style = { { padding : 16 } } >
< MiniCalendar selected = { selDate } onChange = { d => { setSelDate ( d ) ; setPanel ( 'calendar' ) ; } } eventDates = { eventDates } / >
< / div >
< / div >
) }
{ /* Right: calendar view or panel */ }
< div style = { { flex : 1 , overflow : 'auto ' , background : 'var(--background)' } } >
{ /* Right panel */ }
< div style = { { flex : 1 , display : 'flex ' , flexDirection : 'column' , overflow : 'hidden' , minWidth : 0 } } >
{ /* View toolbar */ }
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , padding : '8px 16px' , borderBottom : '1px solid var(--border)' , background : 'var(--surface)' , flexShrink : 0 , flexWrap : 'nowrap' } } >
{ /* Mobile title + create */ }
{ isMobile && (
< span style = { { fontSize : 15 , fontWeight : 700 , flex : 1 } } > Team Schedule < / span >
) }
{ ! isMobile && (
< >
< button className = "btn btn-secondary btn-sm" onClick = { ( ) => setSelDate ( new Date ( ) ) } > Today < / button >
< div style = { { display : 'flex' , gap : 2 } } >
< button className = "btn-icon" onClick = { ( ) => navDate ( - 1 ) } style = { { fontSize : 16 , padding : '2px 8px' } } > ‹ < / button >
< button className = "btn-icon" onClick = { ( ) => navDate ( 1 ) } style = { { fontSize : 16 , padding : '2px 8px' } } > › < / button >
< / div >
{ view !== 'schedule' && < span style = { { fontSize : 13 , fontWeight : 600 , color : 'var(--text-primary)' , whiteSpace : 'nowrap' } } > { navLabel ( ) } < / span > }
< div style = { { marginLeft : 'auto' } } / >
< / >
) }
{ /* View switcher */ }
< div style = { { display : 'flex' , gap : 2 , background : 'var(--surface-variant)' , borderRadius : 'var(--radius)' , padding : 3 , flexShrink : 0 } } >
{ allowedViews . map ( v => {
const labels = { schedule : 'Schedule' , day : 'Day' , week : 'Week' , month : 'Month' } ;
return (
< button key = { v } onClick = { ( ) => { setView ( v ) ; setPanel ( 'calendar' ) ; } } style = { { padding : '4px 10px' , borderRadius : 5 , border : 'none' , cursor : 'pointer' , fontSize : 12 , fontWeight : 600 , background : view === v ? 'var(--surface)' : 'transparent' , color : view === v ? 'var(--text-primary)' : 'var(--text-tertiary)' , boxShadow : view === v ? '0 1px 3px rgba(0,0,0,0.1)' : 'none' , transition : 'all 0.15s' , whiteSpace : 'nowrap' } } >
{ labels [ v ] }
< / button >
) ;
} ) }
< / div >
< / div >
{ /* Mobile date picker */ }
{ isMobile && (
< MobileDatePicker selected = { selDate } onChange = { d => { setSelDate ( d ) ; setPanel ( 'calendar' ) ; } } eventDates = { eventDates } / >
) }
{ /* Calendar or panel content */ }
< div style = { { flex : 1 , overflowY : 'auto' , overflowX : panel === 'eventForm' ? 'auto' : 'hidden' } } >
{ panel === 'calendar' && view === 'schedule' && < ScheduleView events = { events } selectedDate = { selDate } onSelect = { openDetail } / > }
{ panel === 'calendar' && view === 'day' && < DayView events = { events } selectedDate = { selDate } onSelect = { openDetail } / > }
{ panel === 'calendar' && view === 'week' && < WeekView events = { events } selectedDate = { selDate } onSelect = { openDetail } / > }
{ panel === 'calendar' && view === 'month' && < MonthView events = { events } selectedDate = { selDate } onSelect = { openDetail } onSelectDay = { d => { setSelDate ( d ) ; setView ( 'schedule' ) ; } } / > }
{ panel === 'eventForm' && (
< div style = { { padding : 28 , maxWidth : 680 } } >
< h2 style = { { fontSize : 18 , fontWeight : 700 , marginBottom : 24 } } > { editingEvent ? 'Edit Event' : 'New Event' } < / h2 >
{ panel === 'eventForm' && isToolManager && (
< div style = { { padding : 28 , maxWidth : 1024 } } >
< h2 style = { { fontSize : 17 , fontWeight : 700 , marginBottom : 24 } } > { editingEvent ? 'Edit Event' : 'New Event' } < / h2 >
< EventForm event = { editingEvent } userGroups = { userGroups } eventTypes = { eventTypes } selectedDate = { selDate } isToolManager = { isToolManager }
onSave = { handleSaved } onCancel = { ( ) => { setPanel ( 'calendar' ) ; setEditingEvent ( null ) ; } } onDelete = { handleDelete } / >
< / div >
) }
{ panel === 'eventTypes' && (
{ panel === 'eventTypes' && isToolManager && (
< div style = { { padding : 28 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 , marginBottom : 24 } } >
< h2 style = { { fontSize : 18 , fontWeight : 700 , margin : 0 } } > Event Types < / h2 >
< h2 style = { { fontSize : 17 , fontWeight : 700 , margin : 0 } } > Event Types < / h2 >
< button className = "btn btn-secondary btn-sm" onClick = { ( ) => setPanel ( 'calendar' ) } > ← Back < / button >
< / div >
< EventTypesPanel eventTypes = { eventTypes } userGroups = { userGroups } onUpdated = { load } / >
< / div >
) }
{ panel === 'bulkImport' && (
{ panel === 'bulkImport' && isToolManager && (
< div style = { { padding : 28 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 , marginBottom : 24 } } >
< h2 style = { { fontSize : 18 , fontWeight : 700 , margin : 0 } } > Bulk Event Import < / h2 >
< h2 style = { { fontSize : 17 , fontWeight : 700 , margin : 0 } } > Bulk Event Import < / h2 >
< button className = "btn btn-secondary btn-sm" onClick = { ( ) => setPanel ( 'calendar' ) } > ← Back < / button >
< / div >
< BulkImportPanel onImported = { ( ) => { load ( ) ; setPanel ( 'calendar' ) ; } } onCancel = { ( ) => setPanel ( 'calendar' ) } / >
@@ -670,7 +635,6 @@ export default function SchedulePage({ onBack, isToolManager }) {
< EventDetailModal
event = { detailEvent }
isToolManager = { isToolManager }
currentUserId = { user ? . id }
onClose = { ( ) => setDetailEvent ( null ) }
onEdit = { ( ) => { setEditingEvent ( detailEvent ) ; setPanel ( 'eventForm' ) ; setDetailEvent ( null ) ; } }
onAvailabilityChange = { ( ) => openDetail ( detailEvent ) }