@@ -0,0 +1,681 @@
import { useState , useEffect , useCallback , useRef } from 'react' ;
import ReactDOM from 'react-dom' ;
import { api } from '../utils/api.js' ;
import { useToast } from '../contexts/ToastContext.jsx' ;
import { useAuth } from '../contexts/AuthContext.jsx' ;
// ── Utilities ─────────────────────────────────────────────────────────────────
const DAYS = [ 'Sun' , 'Mon' , 'Tue' , 'Wed' , 'Thu' , 'Fri' , 'Sat' ] ;
const MONTHS = [ 'January' , 'February' , 'March' , 'April' , 'May' , 'June' , 'July' , 'August' , 'September' , 'October' , 'November' , 'December' ] ;
const SHORT _MONTHS = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
function fmtDate ( d ) { return ` ${ d . getDate ( ) } ${ SHORT _MONTHS [ d . getMonth ( ) ] } ${ d . getFullYear ( ) } ` ; }
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 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 ( ) ; }
function weekStart ( d ) { const r = new Date ( d ) ; r . setDate ( d . getDate ( ) - d . getDay ( ) ) ; r . setHours ( 0 , 0 , 0 , 0 ) ; return r ; }
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 ─────────────────────────────────────────────────────────────
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 ( ) ;
const cells = [ ] ; for ( let i = 0 ; i < first ; i + + ) cells.push ( null ) ; for ( let d = 1 ; d < = total ; d + + ) cells.push ( d ) ;
return (
< div style = { { userSelect : 'none' } } >
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , marginBottom : 8 , fontSize : 13 , fontWeight : 600 } } >
< button style = { { background : 'none' , border : 'none' , cursor : 'pointer' , padding : '2px 8px' , color : 'var(--text-secondary)' , fontSize : 16 } } onClick = { ( ) => { const n = new Date ( cur ) ; n . setMonth ( m - 1 ) ; setCur ( n ) ; } } > ‹ < / button >
< span > { MONTHS [ m ] } { y } < / span >
< button style = { { background : 'none' , border : 'none' , cursor : 'pointer' , padding : '2px 8px' , color : 'var(--text-secondary)' , fontSize : 16 } } onClick = { ( ) => { const n = new Date ( cur ) ; n . setMonth ( m + 1 ) ; setCur ( n ) ; } } > › < / button >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(7,1fr)' , gap : 1 , fontSize : 11 } } >
{ 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 ) } style = { { textAlign : 'center' , padding : '3px 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 : 1 , left : '50%' , transform : 'translateX(-50%)' , width : 4 , height : 4 , borderRadius : '50%' , background : 'var(--primary)' , display : 'block' } } / > }
< / div >
) ;
} ) }
< / div >
< / div >
) ;
}
// ── Event Type Popup ──────────────────────────────────────────────────────────
function EventTypePopup ( { userGroups , onSave , onClose , editing = null } ) {
const toast = useToast ( ) ;
const DUR = [ 1 , 1.5 , 2 , 2.5 , 3 , 3.5 , 4 , 4.5 , 5 ] ;
const [ name , setName ] = useState ( editing ? . name || '' ) ;
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 [ 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 ) ; }
} ;
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 : 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 >
) ;
}
// ── Event Form ────────────────────────────────────────────────────────────────
function EventForm ( { event , userGroups , eventTypes , selectedDate , onSave , onCancel , onDelete , isToolManager } ) {
const toast = useToast ( ) ;
const def = selectedDate ? selectedDate . toISOString ( ) . slice ( 0 , 10 ) : new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
const [ title , setTitle ] = useState ( event ? . title || '' ) ;
const [ typeId , setTypeId ] = useState ( event ? . event _type _id || '' ) ;
const [ sd , setSd ] = useState ( event ? toDateIn ( event . start _at ) : def ) ;
const [ st , setSt ] = useState ( event ? toTimeIn ( event . start _at ) : '09:00' ) ;
const [ ed , setEd ] = useState ( event ? toDateIn ( event . end _at ) : def ) ;
const [ et , setEt ] = useState ( event ? toTimeIn ( event . end _at ) : '10:00' ) ;
const [ allDay , setAllDay ] = useState ( ! ! event ? . all _day ) ;
const [ loc , setLoc ] = useState ( event ? . location || '' ) ;
const [ desc , setDesc ] = useState ( event ? . description || '' ) ;
const [ pub , setPub ] = useState ( event ? ! ! event . is _public : true ) ;
const [ track , setTrack ] = useState ( ! ! event ? . track _availability ) ;
const [ groups , setGroups ] = 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 ) ;
useEffect ( ( ) => {
if ( ! typeId || event ) return ;
const typ = localTypes . find ( t => t . id === Number ( typeId ) ) ;
if ( ! typ || ! 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 ) setGroups ( prev => new Set ( [ ... prev , 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 ; } ) ;
const handle = async ( ) => {
if ( ! title . trim ( ) ) return toast ( 'Title required' , 'error' ) ;
if ( ! allDay && ( ! sd || ! st || ! ed || ! et ) ) return toast ( 'Start and end required' , '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 ) ; }
} ;
const Row = ( { label , children } ) => (
< div style = { { display : 'flex' , alignItems : 'flex-start' , gap : 16 , marginBottom : 14 } } >
< div style = { { width : 90 , flexShrink : 0 , fontSize : 13 , color : 'var(--text-tertiary)' , paddingTop : 8 , textAlign : 'right' } } > { label } < / div >
< div style = { { flex : 1 } } > { children } < / div >
< / div >
) ;
return (
< div style = { { display : 'flex' , flexDirection : 'column' , maxWidth : 640 } } >
< 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 } } / >
< / 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
< / 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 >
: 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 = { groups . 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 } } > { groups . 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 >
< / 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 >
< 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 >
< button className = "btn btn-secondary btn-sm" onClick = { onCancel } > Cancel < / button >
{ event && isToolManager && < button className = "btn btn-sm" style = { { marginLeft : 'auto' , background : 'var(--error)' , color : 'white' } } onClick = { ( ) => onDelete ( event ) } > Delete < / button > }
< / div >
< / div >
) ;
}
// ── Event Detail Modal (portal) ───────────────────────────────────────────────
function EventDetailModal ( { event , onClose , onEdit , onAvailabilityChange , isToolManager , currentUserId } ) {
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' ) ; }
} ;
return ReactDOM . createPortal (
< div className = "modal-overlay" onClick = { e => e . target === e . currentTarget && onClose ( ) } >
< div className = "modal" style = { { maxWidth : 540 , maxHeight : '88vh' , overflowY : 'auto' } } >
{ /* Header */ }
< 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 } } >
{ event . event _type && < span style = { { width : 13 , height : 13 , borderRadius : '50%' , background : event . event _type . colour , flexShrink : 0 , display : 'inline-block' } } / > }
< h2 style = { { fontSize : 20 , fontWeight : 700 , margin : 0 } } > { event . title } < / h2 >
< / div >
< div style = { { fontSize : 13 , color : 'var(--text-secondary)' , display : 'flex' , alignItems : 'center' , gap : 8 } } >
{ event . event _type ? . name && < span > { event . event _type . name } < / span > }
{ ! event . is _public && < span style = { { background : 'var(--surface-variant)' , borderRadius : 10 , padding : '1px 8px' , fontSize : 11 } } > Private < / span > }
< / div >
< / div >
< div style = { { display : 'flex' , gap : 6 , flexShrink : 0 } } >
{ isToolManager && < button className = "btn btn-secondary btn-sm" onClick = { ( ) => { onClose ( ) ; onEdit ( ) ; } } > Edit < / button > }
< button className = "btn-icon" onClick = { onClose } > < svg width = "18" height = "18" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < / svg > < / button >
< / 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 >
) }
{ /* 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 : '8px 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 >
< / div >
{ avail . length > 0 && (
< div style = { { border : '1px solid var(--border)' , borderRadius : 'var(--radius)' , overflow : 'hidden' } } >
{ avail . map ( r => (
< div key = { r . user _id } style = { { display : 'flex' , alignItems : 'center' , gap : 10 , padding : '8px 12px' , borderBottom : '1px solid var(--border)' , fontSize : 13 } } >
< span style = { { width : 9 , height : 9 , borderRadius : '50%' , background : RESP _COLOR [ r . response ] , flexShrink : 0 , display : 'inline-block' } } / >
< span style = { { flex : 1 } } > { r . display _name || r . name } < / span >
< span style = { { color : RESP _COLOR [ r . response ] , fontSize : 12 , fontWeight : 600 } } > { RESP _LABEL [ r . response ] } < / span >
< / div >
) ) }
< / div >
) }
< / >
) }
< / div >
) }
< / div >
< / div > ,
document . body
) ;
}
// ── Event Types Manager Panel ─────────────────────────────────────────────────
function EventTypesPanel ( { eventTypes , userGroups , onUpdated } ) {
const toast = useToast ( ) ;
const [ editingType , setEditingType ] = useState ( null ) ;
const [ showForm , setShowForm ] = useState ( false ) ;
const handleDel = async et => {
if ( ! confirm ( ` Delete " ${ et . name } "? ` ) ) return ;
try { await api . deleteEventType ( et . id ) ; toast ( 'Deleted' , 'success' ) ; onUpdated ( ) ; } catch ( e ) { toast ( e . message , 'error' ) ; }
} ;
return (
< 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 >
< 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 ? (
< 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 > }
< / div >
) ) }
< / div >
< / div >
) ;
}
// ── Bulk Import Panel ─────────────────────────────────────────────────────────
function BulkImportPanel ( { onImported , onCancel } ) {
const toast = useToast ( ) ;
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 ) ; }
} ;
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 >
< / >
) }
< / div >
) ;
}
// ── 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 >
) ;
} ) ;
}
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 >
) ;
}
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 >
) ;
}
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 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 >
) ;
}
// ── Main Schedule Page ────────────────────────────────────────────────────────
export default function SchedulePage ( { onBack , isToolManager } ) {
const { user } = useAuth ( ) ;
const toast = useToast ( ) ;
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 [ editingEvent , setEditingEvent ] = useState ( null ) ;
const [ detailEvent , setDetailEvent ] = useState ( null ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ createOpen , setCreateOpen ] = useState ( false ) ;
const createRef = useRef ( null ) ;
const load = useCallback ( ( ) => {
Promise . all ( [ api . getEvents ( ) , api . getEventTypes ( ) , api . getUserGroups ( ) ] )
. then ( ( [ ev , et , ug ] ) => { setEvents ( ev . events || [ ] ) ; setEventTypes ( et . eventTypes || [ ] ) ; setUserGroups ( ug . groups || [ ] ) ; setLoading ( false ) ; } )
. catch ( ( ) => setLoading ( false ) ) ;
} , [ ] ) ;
useEffect ( ( ) => { load ( ) ; } , [ load ] ) ;
useEffect ( ( ) => {
if ( ! createOpen ) return ;
const h = e => { if ( createRef . current && ! createRef . current . contains ( e . target ) ) setCreateOpen ( false ) ; } ;
document . addEventListener ( 'mousedown' , h ) ;
return ( ) => document . removeEventListener ( 'mousedown' , h ) ;
} , [ createOpen ] ) ;
const eventDates = new Set ( events . map ( e => e . start _at ? . slice ( 0 , 10 ) ) ) ;
const navDate = dir => {
const d = new Date ( selDate ) ;
if ( view === 'day' ) d . setDate ( d . getDate ( ) + dir ) ;
else if ( view === 'week' ) d . setDate ( d . getDate ( ) + dir * 7 ) ;
else d . setMonth ( d . getMonth ( ) + dir ) ;
setSelDate ( d ) ;
} ;
const navLabel = ( ) => {
if ( view === 'day' ) return ` ${ DAYS [ selDate . getDay ( ) ] } ${ selDate . getDate ( ) } ${ MONTHS [ selDate . getMonth ( ) ] } ${ selDate . getFullYear ( ) } ` ;
if ( view === 'week' ) { const ws = weekStart ( selDate ) , we = new Date ( ws ) ; we . setDate ( we . getDate ( ) + 6 ) ; return ` ${ SHORT _MONTHS [ ws . getMonth ( ) ] } ${ ws . getDate ( ) } – ${ SHORT _MONTHS [ we . getMonth ( ) ] } ${ we . getDate ( ) } ${ we . getFullYear ( ) } ` ; }
return ` ${ MONTHS [ selDate . getMonth ( ) ] } ${ selDate . getFullYear ( ) } ` ;
} ;
const openDetail = async e => {
try { const { event } = await api . getEvent ( e . id ) ; setDetailEvent ( event ) ; } catch { toast ( 'Failed to load event' , 'error' ) ; }
} ;
const handleSaved = ( ) => { load ( ) ; setPanel ( 'calendar' ) ; setEditingEvent ( null ) ; } ;
const handleDelete = async e => {
if ( ! confirm ( ` Delete " ${ e . title } "? ` ) ) return ;
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 >
) ;
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 >
{ /* Create dropdown */ }
{ 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" 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)' } } >
{ [ [ 'Event' , ( ) => { setPanel ( 'eventForm' ) ; setEditingEvent ( null ) ; setCreateOpen ( false ) ; } ] ,
[ 'Event Type' , ( ) => { setPanel ( 'eventTypes' ) ; setCreateOpen ( false ) ; } ] ,
[ 'Bulk Event Import' , ( ) => { setPanel ( 'bulkImport' ) ; setCreateOpen ( false ) ; } ]
] . map ( ( [ label , action ] ) => (
< button key = { label } onClick = { action } style = { { display : 'block' , width : '100%' , padding : '9px 16px' , textAlign : 'left' , fontSize : 14 , background : 'none' , border : 'none' , cursor : 'pointer' , color : 'var(--text-primary)' } }
onMouseEnter = { e => e . currentTarget . style . background = 'var(--background)' } onMouseLeave = { e => e . currentTarget . style . background = '' } > { label } < / button >
) ) }
< / div >
) }
< / 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 mini-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' } } >
< MiniCalendar selected = { selDate } onChange = { d => { setSelDate ( d ) ; setPanel ( 'calendar' ) ; } } eventDates = { eventDates } / >
< / div >
{ /* Right: calendar view or panel */ }
< div style = { { flex : 1 , overflow : 'auto' , background : 'var(--background)' } } >
{ 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 >
< EventForm event = { editingEvent } userGroups = { userGroups } eventTypes = { eventTypes } selectedDate = { selDate } isToolManager = { isToolManager }
onSave = { handleSaved } onCancel = { ( ) => { setPanel ( 'calendar' ) ; setEditingEvent ( null ) ; } } onDelete = { handleDelete } / >
< / div >
) }
{ panel === 'eventTypes' && (
< 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 >
< button className = "btn btn-secondary btn-sm" onClick = { ( ) => setPanel ( 'calendar' ) } > ← Back < / button >
< / div >
< EventTypesPanel eventTypes = { eventTypes } userGroups = { userGroups } onUpdated = { load } / >
< / div >
) }
{ panel === 'bulkImport' && (
< 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 >
< button className = "btn btn-secondary btn-sm" onClick = { ( ) => setPanel ( 'calendar' ) } > ← Back < / button >
< / div >
< BulkImportPanel onImported = { ( ) => { load ( ) ; setPanel ( 'calendar' ) ; } } onCancel = { ( ) => setPanel ( 'calendar' ) } / >
< / div >
) }
< / div >
< / div >
{ /* Event detail modal */ }
{ detailEvent && (
< EventDetailModal
event = { detailEvent }
isToolManager = { isToolManager }
currentUserId = { user ? . id }
onClose = { ( ) => setDetailEvent ( null ) }
onEdit = { ( ) => { setEditingEvent ( detailEvent ) ; setPanel ( 'eventForm' ) ; setDetailEvent ( null ) ; } }
onAvailabilityChange = { ( ) => openDetail ( detailEvent ) }
/ >
) }
< / div >
) ;
}