@@ -159,25 +159,45 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
const [ localTypes , setLocalTypes ] = useState ( eventTypes ) ;
const typeRef = useRef ( null ) ;
// When event type changes: auto set duration and default group
// Track whether the user has manually changed the end time (vs auto-computed)
const userSetEndTime = useRef ( ! ! event ) ; // editing mode: treat saved end as user-set
// When event type changes:
// - Creating: always apply the type's duration to compute end time
// - Editing: only apply duration if the type HAS a defined duration
// (if no duration on type, keep existing saved end time)
useEffect ( ( ) => {
if ( ! sd || ! st ) return ;
const typ = localTypes . find ( t => t . id === Number ( typeId ) ) ;
const dur = typ ? . default _duration _hrs || 1 ;
const start = buildISO ( sd , st ) ;
if ( start ) {
if ( ! start ) return ;
if ( ! event ) {
// Creating new event — always apply duration (default 1hr)
const dur = typ ? . default _duration _hrs || 1 ;
setEd ( toDateIn ( addHours ( start , dur ) ) ) ;
setEt ( toTimeIn ( addHours ( start , dur ) ) ) ;
userSetEndTime . current = false ;
} else {
// Editing — only update end time if the new type has an explicit duration
if ( typ ? . default _duration _hrs ) {
setEd ( toDateIn ( addHours ( start , typ . default _duration _hrs ) ) ) ;
setEt ( toTimeIn ( addHours ( start , typ . default _duration _hrs ) ) ) ;
userSetEndTime . current = false ;
}
// else: keep existing saved end time — do nothing
}
if ( typ ? . default _user _group _id && ! event ) setGrps ( prev => new Set ( [ ... prev , Number ( typ . default _user _group _id ) ] ) ) ;
} , [ typeId ] ) ;
// When start date changes: auto- match end date
useEffect ( ( ) => { if ( ! event ) setEd ( sd ) ; } , [ sd ] ) ;
// When start date changes: match end date (both modes) unless user set it manually
useEffect ( ( ) => {
if ( ! userSetEndTime . current ) setEd ( sd ) ;
} , [ sd ] ) ;
// When start time changes: auto-update end time preserving duration
// When start time changes: recompute end using current duration offset
useEffect ( ( ) => {
if ( ! sd || ! st ) return ;
if ( userSetEndTime . current ) return ; // user already picked a specific end time — respect it
const typ = localTypes . find ( t => t . id === Number ( typeId ) ) ;
const dur = typ ? . default _duration _hrs || 1 ;
const start = buildISO ( sd , st ) ;
@@ -238,10 +258,10 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
{ 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 } } >
< select className = "input" value = { et } onChange = { e => { setEt ( e . target . value ) ; userSetEndTime . current = true ; } } 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 } } / >
< input type = "date" className = "input" value = { ed } onChange = { e => { setEd ( e . target . value ) ; userSetEndTime . current = true ; } } style = { { width : 150 , flexShrink : 0 } } / >
< / >
) }
< / div >
@@ -467,11 +487,13 @@ function ScheduleView({ events, selectedDate, onSelect }) {
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 > ) ; } ) } < / > ;
}
const HOUR _H = 56 ; // px per hour row
const HOUR _H = 52 ; // px per hour row
const DAY _START = 0 ; // show from midnight
const DAY _END = 24 ; // to midnight
function eventTopOffset ( startDate ) {
const h = startDate . getHours ( ) , m = startDate . getMinutes ( ) ;
return ( h - 7 ) * HOUR _H + ( m / 60 ) * HOUR _H ;
return ( h - DAY _START ) * HOUR _H + ( m / 60 ) * HOUR _H ;
}
function eventHeightPx ( startDate , endDate ) {
const diffMs = endDate - startDate ;
@@ -480,41 +502,42 @@ function eventHeightPx(startDate, endDate) {
}
function DayView ( { events , selectedDate , onSelect } ) {
const hours = Array . from ( { length : 16 } , ( _ , i ) => i + 7 ) ; // 7am– 10pm
const hours = Array . from ( { length : DAY _END - DAY _START } , ( _ , i ) => i + DAY _START ) ;
const day = events . filter ( e => sameDay ( new Date ( e . start _at ) , selectedDate ) ) ;
const scrollRef = useRef ( null ) ;
useEffect ( ( ) => { if ( scrollRef . current ) scrollRef . current . scrollTop = 7 * HOUR _H ; } , [ selectedDate ] ) ;
const fmtHour = h => h === 0 ? '12 AM' : h < 12 ? ` $ { h } AM ` : h = = = 12 ? ' 12 PM ' : ` $ { h - 12 } PM ` ;
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 = { { display : 'flex' , flexDirection : 'column' , height : '100%' } } >
< div style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , padding : '8px 0 8px 60px' , fontSize : 13 , fontWeight : 600 , color : 'var(--primary)' , flexShrink : 0 }} >
< div style = { { textAlign : 'center' } } > < div > { DAYS [ selectedDate . getDay ( ) ] } < / div > < div style = { { fontSize : 28 , fontWeight : 700 } } > { selectedDate . getDate ( ) } < / div > < / div >
< / div >
< div style = { { position : 'relative' } } >
{ /* Hour grid */ }
{ hours . map ( h => (
< div key = { h } style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , height : HOUR _H } } >
< 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 ref = { scrollRef } style = { { flex : 1 , overflowY : 'auto' , position : 'relative' } } >
< div style = { { position : 'relative' } } >
{ hours . map ( h => (
< div key = { h } style = { { display : 'flex' , borderBottom : '1px solid var(--border)' , height : HOUR _H } } >
< div style = { { width : 60 , flexShrink : 0 , fontSize : 11 , color : 'var(--text-tertiary)' , padding : '3px 10px 0' , textAlign : 'right' } } > { fmtHour ( h ) } < / div >
< div style = { { flex : 1} } / >
< / div >
< div style = { { flex : 1 } } / >
< / div >
) ) }
{ /* Event blocks — absolutely positioned */ }
{ day . map ( e => {
const s = new Date ( e . start _at ) , en = new Date ( e . end _at ) ;
const top = eventTopOffset ( s ) , height = eventHeightPx ( s , en ) ;
return (
< div key = { e . id } onClick = { ( ) => onSelect ( e ) } style = { {
position : 'absolute' , left : 64 , right : 8 ,
top , height ,
background : e . event _type ? . colour || '#6366f1' , color : 'white ',
borderRadius : 5 , padding : '3px 8px' , cursor : 'pointer' ,
fontSize : 12 , fontWeight : 600 , overflow : 'hidden' ,
boxShadow : '0 1px 3px rgba(0,0,0,0.2)' ,
} } >
< div style = { { whiteSpace : 'nowrap' , overflow : 'hidden' , textOverflow : 'ellipsis' } } > { e . title } < / div >
{ height > 32 && < div style = { { fontSize : 10 , opacity : 0.85 } } > { fmtRange ( e . start _at , e . end _at ) } < / div > }
< / div >
) ;
} ) }
) ) }
{ day . map ( e => {
const s = new Date ( e . start _at ) , en = new Date ( e . end _at ) ;
const top = eventTopOffset ( s ) , height = eventHeightPx ( s , en ) ;
return (
< div key = { e . id } onClick = { ( ) => onSelect ( e ) } style = { {
position : 'absolute' , left : 64 , right : 8 ,
top , height ,
background : e . event _type ? . colour || '#6366f1' , color : 'white' ,
borderRadius : 5 , padding : '3px 8px' , cursor : 'pointer' ,
fontSize : 12 , fontWeight : 600 , overflow : 'hidden' ,
boxShadow : '0 1px 3px rgba(0,0,0,0.2) ' ,
} } >
< div style = { { whiteSpace : 'nowrap' , overflow : 'hidden' , textOverflow : 'ellipsis' } } > { e . title } < / div >
{ height > 32 && < div style = { { fontSize : 10 , opacity : 0.85 } } > { fmtRange ( e . start _at , e . end _at ) } < / div > }
< / div >
) ;
} ) }
< / div >
< / div >
< / div >
) ;
@@ -522,22 +545,24 @@ function DayView({ events, selectedDate, onSelect }) {
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 ( ) ;
const hours = Array . from ( { length : DAY _END - DAY _START } , ( _ , i ) => i + DAY _START ) , today = new Date ( ) ;
const scrollRef = useRef ( null ) ;
useEffect ( ( ) => { if ( scrollRef . current ) scrollRef . current . scrollTop = 7 * HOUR _H ; } , [ selectedDate ] ) ;
const fmtHour = h => h === 0 ? '12 AM' : h < 12 ? ` $ { h } AM ` : h = = = 12 ? ' 12 PM ' : ` $ { h - 12 } PM ` ;
return (
< div>
< div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' } } >
{ /* Day headers */ }
< div style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , borderBottom : '1px solid var(--border)' , position : 'sticky' , top : 0 , background: 'var(--surface)' , zIndex : 2 } } >
< div style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , borderBottom : '1px solid var(--border)' , background : 'var(--surface)' , flexShrink : 0 } } >
< 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 >
{ /* Time grid with event columns */ }
{ /* Scrollable time grid */ }
< div ref = { scrollRef } style = { { flex : 1 , overflowY : 'auto' } } >
< div style = { { display : 'grid' , gridTemplateColumns : '60px repeat(7,1fr)' , position : 'relative' } } >
{ /* Time labels column */ }
< div >
{ hours . map ( h => (
< div key = { h } style = { { height : HOUR _H , borderBottom : '1px solid var(--border)' , 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 key = { h } style = { { height : HOUR _H , borderBottom : '1px solid var(--border)' , fontSize : 11 , color : 'var(--text-tertiary)' , padding : '3px 10px 0' , textAlign : 'right' } } > { fmtHour ( h ) } < / div >
) ) }
< / div >
{ /* Day columns */ }
@@ -566,6 +591,7 @@ function WeekView({ events, selectedDate, onSelect }) {
) ;
} ) }
< / div >
< / div >
< / div >
) ;
}