Skip to content

Commit e974458

Browse files
author
=
committed
Fix mobile plan override sheets
1 parent b2a71ce commit e974458

7 files changed

Lines changed: 233 additions & 26 deletions

File tree

‎apps/predbat/frontend.zip‎

433 Bytes
Binary file not shown.

‎frontend/src/components/PlanTable.css‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -724,12 +724,14 @@
724724
position: relative;
725725
}
726726

727-
.plan-rate-control {
727+
.plan-rate-control,
728+
.plan-load-control {
728729
position: relative;
729730
display: inline-block;
730731
}
731732

732-
.plan-rate-button {
733+
.plan-rate-button,
734+
.plan-load-button {
733735
display: inline-flex;
734736
align-items: center;
735737
gap: 0.3rem;
@@ -744,11 +746,14 @@
744746
}
745747

746748
.plan-rate-button:hover,
747-
.plan-rate-button:focus-visible {
749+
.plan-rate-button:focus-visible,
750+
.plan-load-button:hover,
751+
.plan-load-button:focus-visible {
748752
background: color-mix(in srgb, var(--color-text) 6%, transparent);
749753
}
750754

751-
.plan-rate-button.is-manual {
755+
.plan-rate-button.is-manual,
756+
.plan-load-button.is-manual {
752757
border-color: color-mix(in srgb, var(--color-text-muted) 35%, transparent);
753758
}
754759

@@ -946,10 +951,11 @@
946951

947952
.plan-cell-popover {
948953
position: fixed;
949-
inset: auto 0 0;
954+
inset: auto 0 var(--plan-mobile-keyboard-offset, 0px);
950955
z-index: 110;
951956
width: auto;
952957
max-width: none;
958+
max-height: calc(100dvh - var(--plan-mobile-keyboard-offset, 0px));
953959
padding: 0.8rem;
954960
padding-bottom: max(0.8rem, env(safe-area-inset-bottom));
955961
border-radius: 1rem 1rem 0 0;

‎frontend/src/components/PlanTable.tsx‎

Lines changed: 213 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
483656
type 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">

‎frontend/src/pages/ComparePage.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const colours = ['#2563eb', '#f59e0b', '#059669', '#dc2626', '#7c3aed', '#0891b2
6363
const emptyOverrides: PlanOverrides = {
6464
manual_charge_times: [], manual_export_times: [], manual_freeze_charge_times: [],
6565
manual_freeze_export_times: [], manual_demand_times: [], manual_import_rates: [],
66-
manual_export_rates: [], manual_soc: []
66+
manual_export_rates: [], manual_load_adjust: [], manual_soc: []
6767
}
6868

6969
function value(value: number | null, suffix = '') {

‎frontend/src/pages/PlanPage.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const EMPTY_OVERRIDES: PlanOverrides = {
2626
manual_demand_times: [],
2727
manual_import_rates: [],
2828
manual_export_rates: [],
29+
manual_load_adjust: [],
2930
manual_soc: []
3031
}
3132

‎frontend/src/types/plan.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ export type ManualRateOverride = {
104104
rate: number
105105
}
106106

107+
export type ManualLoadOverride = {
108+
minutes: number
109+
adjustment: number
110+
}
111+
107112
export type ManualSocOverride = {
108113
minutes: number
109114
target: number
@@ -118,6 +123,7 @@ export type PlanOverrides = {
118123

119124
manual_import_rates: ManualRateOverride[]
120125
manual_export_rates: ManualRateOverride[]
126+
manual_load_adjust: ManualLoadOverride[]
121127

122128
manual_soc: ManualSocOverride[]
123129
}

‎frontend/tests/plan-debug.test.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ const overrides = {
123123
manual_demand_times: [],
124124
manual_import_rates: [],
125125
manual_export_rates: [],
126+
manual_load_adjust: [],
126127
manual_soc: []
127128
}
128129

0 commit comments

Comments
 (0)