@@ -480,6 +480,179 @@ function RateCell({
480480 )
481481}
482482
483+ type LoadCellProps = {
484+ row : PlanRow
485+ overrides : PlanOverrides
486+ isPast : boolean
487+ debugEnabled : boolean
488+ open : boolean
489+ onToggle : ( ) => void
490+ onClose : ( ) => void
491+ onOverrideSubmitted : ( ) => void
492+ }
493+
494+ function LoadCell ( {
495+ row,
496+ overrides,
497+ isPast,
498+ debugEnabled,
499+ open,
500+ onToggle,
501+ onClose,
502+ onOverrideSubmitted
503+ } : LoadCellProps ) {
504+ const override = ( overrides . manual_load_adjust ?? [ ] ) . find ( ( item ) => item . minutes === row . slot_minute ) ?? null
505+ const [ value , setValue ] = useState ( '' )
506+ const [ saving , setSaving ] = useState ( false )
507+ const [ error , setError ] = useState < string | null > ( null )
508+ const controlRef = useRef < HTMLDivElement | null > ( null )
509+
510+ useEffect ( ( ) => {
511+ if ( ! open ) {
512+ return
513+ }
514+
515+ function handlePointerDown ( event : PointerEvent ) {
516+ if ( controlRef . current && ! controlRef . current . contains ( event . target as Node ) ) {
517+ onClose ( )
518+ }
519+ }
520+
521+ function handleKeyDown ( event : KeyboardEvent ) {
522+ if ( event . key === 'Escape' ) {
523+ onClose ( )
524+ }
525+ }
526+
527+ document . addEventListener ( 'pointerdown' , handlePointerDown )
528+ document . addEventListener ( 'keydown' , handleKeyDown )
529+
530+ return ( ) => {
531+ document . removeEventListener ( 'pointerdown' , handlePointerDown )
532+ document . removeEventListener ( 'keydown' , handleKeyDown )
533+ }
534+ } , [ open , onClose ] )
535+
536+ function openEditor ( ) {
537+ if ( isPast ) {
538+ return
539+ }
540+
541+ if ( ! open ) {
542+ setValue ( String ( override ?. adjustment ?? row . load_forecast ) )
543+ setError ( null )
544+ }
545+
546+ onToggle ( )
547+ }
548+
549+ async function sendOverride ( action : 'Set Load' | 'Clear Load' , adjustment : number ) {
550+ setSaving ( true )
551+ setError ( null )
552+
553+ const formData = new FormData ( )
554+ formData . append ( 'time' , formatOverrideTime ( row . time ) )
555+ formData . append ( 'action' , action )
556+ formData . append ( 'rate' , String ( adjustment ) )
557+
558+ try {
559+ const response = await fetch ( './rate_override' , {
560+ method : 'POST' ,
561+ body : formData
562+ } )
563+ const result = ( await response . json ( ) ) as { success ?: boolean ; message ?: string }
564+
565+ if ( ! response . ok || result . success === false ) {
566+ throw new Error ( result . message || `HTTP ${ response . status } ` )
567+ }
568+
569+ onClose ( )
570+ onOverrideSubmitted ( )
571+ } catch ( error ) {
572+ const message = error instanceof Error ? error . message : 'Unknown error'
573+ setError ( `Unable to update load forecast: ${ message } ` )
574+ } finally {
575+ setSaving ( false )
576+ }
577+ }
578+
579+ async function setLoad ( event : React . FormEvent ) {
580+ event . preventDefault ( )
581+ const adjustment = Number ( value )
582+
583+ if ( ! Number . isFinite ( adjustment ) ) {
584+ setError ( 'Enter a valid load forecast.' )
585+ return
586+ }
587+
588+ await sendOverride ( 'Set Load' , adjustment )
589+ }
590+
591+ return (
592+ < td
593+ className = { [ 'plan-number' , 'plan-load' , row . load_color ? 'has-plan-cell-colour' : '' ] . filter ( Boolean ) . join ( ' ' ) }
594+ style = { cellColour ( row . load_color ) }
595+ >
596+ < div ref = { controlRef } className = "plan-load-control" >
597+ < button
598+ type = "button"
599+ className = { [ 'plan-load-button' , override ? 'is-manual' : '' ] . filter ( Boolean ) . join ( ' ' ) }
600+ onClick = { openEditor }
601+ aria-disabled = { isPast }
602+ aria-label = { override ? `Load forecast ${ row . load_forecast . toFixed ( 2 ) } kWh, manually overridden` : `Load forecast ${ row . load_forecast . toFixed ( 2 ) } kWh` }
603+ >
604+ < span className = "plan-value-with-indicator" >
605+ { row . load_forecast > 0 && row . load_color && (
606+ < span className = "plan-value-indicator" style = { { backgroundColor : row . load_color } } aria-hidden = "true" />
607+ ) }
608+ < span > { row . load_forecast . toFixed ( 2 ) } </ span >
609+ </ span >
610+
611+ { override && < span className = "plan-rate-manual-dot" title = "Manual load override" aria-hidden = "true" /> }
612+ </ button >
613+
614+ { debugEnabled && ( row . load_forecast10 ?? 0 ) > 0 && (
615+ < small className = "plan-debug-value" > ({ row . load_forecast10 ?. toFixed ( 2 ) } )</ small >
616+ ) }
617+
618+ { open && ! isPast && (
619+ < form className = "plan-cell-popover" onSubmit = { setLoad } >
620+ < div className = "plan-cell-popover-title" > { formatTime ( row . time ) } load override</ div >
621+
622+ < label >
623+ < span > Load forecast</ span >
624+
625+ < div className = "plan-rate-input" >
626+ < input
627+ type = "number"
628+ step = "0.01"
629+ inputMode = "decimal"
630+ value = { value }
631+ onChange = { ( event ) => setValue ( event . target . value ) }
632+ autoFocus
633+ disabled = { saving }
634+ />
635+
636+ < span > kWh</ span >
637+ </ div >
638+ </ label >
639+
640+ < button type = "submit" disabled = { saving } > { saving ? 'Saving…' : 'Set override' } </ button >
641+
642+ { override && (
643+ < button type = "button" className = "plan-rate-clear" disabled = { saving } onClick = { ( ) => void sendOverride ( 'Clear Load' , override . adjustment ) } >
644+ Use Predbat forecast
645+ </ button >
646+ ) }
647+
648+ { error && < div className = "plan-rate-error" role = "alert" > { error } </ div > }
649+ </ form >
650+ ) }
651+ </ div >
652+ </ td >
653+ )
654+ }
655+
483656type TargetCellProps = {
484657 row : PlanRow
485658 overrides : PlanOverrides
@@ -1136,6 +1309,32 @@ export default function PlanTable({
11361309
11371310 const [ openCellEditor , setOpenCellEditor ] = useState < string | null > ( null )
11381311
1312+ useEffect ( ( ) => {
1313+ const visualViewport = window . visualViewport
1314+
1315+ if ( ! visualViewport ) {
1316+ return
1317+ }
1318+
1319+ const viewport = visualViewport as VisualViewport
1320+
1321+ function updateKeyboardOffset ( ) {
1322+ const offset = Math . max ( 0 , window . innerHeight - viewport . height - viewport . offsetTop )
1323+
1324+ document . documentElement . style . setProperty ( '--plan-mobile-keyboard-offset' , `${ offset } px` )
1325+ }
1326+
1327+ updateKeyboardOffset ( )
1328+ viewport . addEventListener ( 'resize' , updateKeyboardOffset )
1329+ viewport . addEventListener ( 'scroll' , updateKeyboardOffset )
1330+
1331+ return ( ) => {
1332+ viewport . removeEventListener ( 'resize' , updateKeyboardOffset )
1333+ viewport . removeEventListener ( 'scroll' , updateKeyboardOffset )
1334+ document . documentElement . style . removeProperty ( '--plan-mobile-keyboard-offset' )
1335+ }
1336+ } , [ ] )
1337+
11391338 function toggleCellEditor ( key : string ) {
11401339 setOpenOverrideTime ( null )
11411340 setOverrideError ( null )
@@ -1743,26 +1942,20 @@ export default function PlanTable({
17431942 ) }
17441943 </ td >
17451944
1746- < td
1747- className = { [ 'plan-number' , 'plan-load' , row . load_color ? 'has-plan-cell-colour' : '' ] . filter ( Boolean ) . join ( ' ' ) }
1748- style = { cellColour ( row . load_color ) }
1749- >
1750- < span className = "plan-value-with-indicator" >
1751- { row . load_forecast > 0 && row . load_color && (
1752- < span
1753- className = "plan-value-indicator"
1754- style = { { backgroundColor : row . load_color } }
1755- aria-hidden = "true"
1756- />
1757- ) }
1758- < span > { row . load_forecast . toFixed ( 2 ) } </ span >
1759- </ span >
1760- { debugEnabled && ( row . load_forecast10 ?? 0 ) > 0 && (
1761- < small className = "plan-debug-value" >
1762- ({ row . load_forecast10 ?. toFixed ( 2 ) } )
1763- </ small >
1764- ) }
1765- </ td >
1945+ < LoadCell
1946+ row = { row }
1947+ overrides = { overrides }
1948+ isPast = { isPast }
1949+ debugEnabled = { debugEnabled }
1950+ onOverrideSubmitted = { onOverrideSubmitted }
1951+ open = { openCellEditor === `load:${ row . time } ` }
1952+ onToggle = { ( ) => {
1953+ toggleCellEditor ( `load:${ row . time } ` )
1954+ } }
1955+ onClose = { ( ) => {
1956+ setOpenCellEditor ( null )
1957+ } }
1958+ />
17661959
17671960 { debugEnabled && (
17681961 < td className = "plan-number plan-debug-column" >
0 commit comments