|
|
|
|
@@ -159,6 +159,7 @@ function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filte
|
|
|
|
|
value={filterKeyword}
|
|
|
|
|
onChange={e=>onFilterKeyword(e.target.value)}
|
|
|
|
|
placeholder="Search events…"
|
|
|
|
|
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
|
|
|
|
|
style={{width:'100%',padding:'7px 8px 7px 28px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:13,boxSizing:'border-box'}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -806,17 +807,18 @@ function DayView({ events, selectedDate, onSelect, onSwipe }) {
|
|
|
|
|
const handleTouchEnd = e => {
|
|
|
|
|
const dx = e.changedTouches[0].clientX - touchRef.current.x;
|
|
|
|
|
const dy = Math.abs(e.changedTouches[0].clientY - touchRef.current.y);
|
|
|
|
|
// Require horizontal swipe > 60px, not too vertical, and not from left edge (< 30px = back gesture)
|
|
|
|
|
if(Math.abs(dx) > 60 && dy < 80 && touchRef.current.x > 30) {
|
|
|
|
|
// Only trigger horizontal swipe if clearly horizontal (dx > dy) and > 60px
|
|
|
|
|
// and not from left edge (< 30px = OS back gesture)
|
|
|
|
|
if(Math.abs(dx) > 60 && Math.abs(dx) > dy * 1.5 && touchRef.current.x > 30) {
|
|
|
|
|
onSwipe?.(dx < 0 ? 1 : -1); // left = next day, right = prev day
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return(
|
|
|
|
|
<div style={{display:'flex',flexDirection:'column',height:'100%'}} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
|
|
|
|
|
<div style={{display:'flex',flexDirection:'column',height:'100%',touchAction:'pan-y'}} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
|
|
|
|
|
<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 ref={scrollRef} style={{flex:1,overflowY:'auto',position:'relative'}}>
|
|
|
|
|
<div ref={scrollRef} style={{flex:1,overflowY:'auto',position:'relative',touchAction:'pan-y'}}>
|
|
|
|
|
<div style={{position:'relative'}}>
|
|
|
|
|
{hours.map(h=>(
|
|
|
|
|
<div key={h} style={{display:'flex',borderBottom:'1px solid var(--border)',height:HOUR_H}}>
|
|
|
|
|
@@ -861,20 +863,21 @@ function WeekView({ events, selectedDate, onSelect }) {
|
|
|
|
|
const handleTouchEnd = e => {
|
|
|
|
|
const dx = e.changedTouches[0].clientX - touchRef.current.x;
|
|
|
|
|
const dy = Math.abs(e.changedTouches[0].clientY - touchRef.current.y);
|
|
|
|
|
// Require horizontal swipe > 60px, not too vertical, and not from left edge (< 30px = back gesture)
|
|
|
|
|
if(Math.abs(dx) > 60 && dy < 80 && touchRef.current.x > 30) {
|
|
|
|
|
// Only trigger horizontal swipe if clearly horizontal (dx > dy) and > 60px
|
|
|
|
|
// and not from left edge (< 30px = OS back gesture)
|
|
|
|
|
if(Math.abs(dx) > 60 && Math.abs(dx) > dy * 1.5 && touchRef.current.x > 30) {
|
|
|
|
|
onSwipe?.(dx < 0 ? 1 : -1); // left = next day, right = prev day
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return(
|
|
|
|
|
<div style={{display:'flex',flexDirection:'column',height:'100%'}} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
|
|
|
|
|
<div style={{display:'flex',flexDirection:'column',height:'100%',touchAction:'pan-y'}} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
|
|
|
|
|
{/* Day headers */}
|
|
|
|
|
<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>
|
|
|
|
|
{/* Scrollable time grid */}
|
|
|
|
|
<div ref={scrollRef} style={{flex:1,overflowY:'auto'}}>
|
|
|
|
|
<div ref={scrollRef} style={{flex:1,overflowY:'auto',touchAction:'pan-y'}}>
|
|
|
|
|
<div style={{display:'grid',gridTemplateColumns:'60px repeat(7,1fr)',position:'relative'}}>
|
|
|
|
|
{/* Time labels column */}
|
|
|
|
|
<div>
|
|
|
|
|
@@ -1049,8 +1052,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|
|
|
|
</button>
|
|
|
|
|
{createOpen && (
|
|
|
|
|
<div style={{ position:'absolute', top:'100%', left:0, right:0, zIndex:100, background:'var(--surface-variant)', border:'1px solid var(--border)', borderRadius:'var(--radius)', marginTop:4, boxShadow:'0 4px 16px rgba(0,0,0,0.18)' }}>
|
|
|
|
|
{[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);}],
|
|
|
|
|
['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);}],
|
|
|
|
|
{[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);setFilterKeyword('');setFilterTypeId('');}],
|
|
|
|
|
['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);setFilterKeyword('');setFilterTypeId('');}],
|
|
|
|
|
['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)'}}
|
|
|
|
|
@@ -1077,6 +1080,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|
|
|
|
placeholder={`Keyword… (space = OR, "phrase")`}
|
|
|
|
|
value={filterKeyword}
|
|
|
|
|
onChange={e=>setFilterKeyword(e.target.value)}
|
|
|
|
|
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
|
|
|
|
|
style={{ marginBottom:8, fontSize:13 }}
|
|
|
|
|
/>
|
|
|
|
|
<select
|
|
|
|
|
@@ -1128,7 +1132,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|
|
|
|
{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' }}>
|
|
|
|
|
<button key={v} onClick={()=>{setView(v);setPanel('calendar');setSelDate(new Date());setFilterKeyword('');setFilterTypeId('');}} 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>
|
|
|
|
|
);
|
|
|
|
|
@@ -1165,7 +1169,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|
|
|
|
<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}/>
|
|
|
|
|
onSave={handleSaved} onCancel={()=>{setPanel('calendar');setEditingEvent(null);setFilterKeyword('');setFilterTypeId('');}} onDelete={handleDelete}/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
@@ -1212,7 +1216,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|
|
|
|
selectedDate={selDate}
|
|
|
|
|
isToolManager={isToolManager}
|
|
|
|
|
onSave={handleSaved}
|
|
|
|
|
onCancel={()=>{setPanel('calendar');setEditingEvent(null);}}
|
|
|
|
|
onCancel={()=>{setPanel('calendar');setEditingEvent(null);setFilterKeyword('');setFilterTypeId('');}}
|
|
|
|
|
onDelete={handleDelete}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -1228,8 +1232,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
|
|
|
|
|
</button>
|
|
|
|
|
{createOpen && (
|
|
|
|
|
<div style={{ position:'absolute', bottom:'calc(100% + 8px)', right:0, zIndex:100, background:'var(--surface-variant)', border:'1px solid var(--border)', borderRadius:'var(--radius)', boxShadow:'0 -4px 16px rgba(0,0,0,0.15)', minWidth:180 }}>
|
|
|
|
|
{[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);}],
|
|
|
|
|
['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);}],
|
|
|
|
|
{[['Event', ()=>{setPanel('eventForm');setEditingEvent(null);setCreateOpen(false);setFilterKeyword('');setFilterTypeId('');}],
|
|
|
|
|
['Event Type', ()=>{setPanel('eventTypes');setCreateOpen(false);setFilterKeyword('');setFilterTypeId('');}],
|
|
|
|
|
].map(([label,action])=>(
|
|
|
|
|
<button key={label} onClick={action} style={{display:'block',width:'100%',padding:'12px 16px',textAlign:'left',fontSize:15,background:'none',border:'none',cursor:'pointer',color:'var(--text-primary)',borderBottom:'1px solid var(--border)'}}
|
|
|
|
|
onMouseEnter={e=>e.currentTarget.style.background='var(--background)'} onMouseLeave={e=>e.currentTarget.style.background=''}>{label}</button>
|
|
|
|
|
|