From a5bfec894e0eae0199dd79921bd74c313e130f17 Mon Sep 17 00:00:00 2001 From: Hank Date: Tue, 7 Jul 2026 13:17:46 -0700 Subject: [PATCH] Fix "$652.89/hr" on Variable-rate meters (display only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Waterfront zones (BS/WB/RV/PB/CB) use RateType "Variable" with a placeholder Rate 652.89 ("Variable Rate - See signs"), not an hourly price. Only render $/hr for the real "Hour" rate type; show "Variable — see signs" otherwise, and derive the headline Rate from the current policy instead of the raw zone Rate ("NA"/652.89). Actual quoting is unaffected — the estimate ladder returns sane per-duration prices. Co-Authored-By: Claude Fable 5 --- app/src/screens/MeterDetailScreen.tsx | 30 +++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/app/src/screens/MeterDetailScreen.tsx b/app/src/screens/MeterDetailScreen.tsx index 4fb3199..72a62fa 100644 --- a/app/src/screens/MeterDetailScreen.tsx +++ b/app/src/screens/MeterDetailScreen.tsx @@ -17,10 +17,28 @@ function fmtMaxTime(m?: number): string { return `${m} min`; } -/** Zone rate comes as a string that sometimes already includes a '$' and spaces. */ +/** Zone rate is a string that may include '$'/spaces, or be "NA" / a placeholder. */ function fmtRate(r?: string): string | undefined { if (r == null) return undefined; - return '$' + String(r).replace(/^\$\s*/, '').trim(); + const n = Number(String(r).replace(/^\$\s*/, '').trim()); + return Number.isFinite(n) ? `$${n.toFixed(2)}` : undefined; +} + +/** + * A human rate label from the CURRENTLY-active policy. Only "Hour" is a real + * hourly price; "Variable" carries a placeholder number (see-signs), so never + * render it as $/hr. + */ +function currentRateLabel(policy: SpacePolicy | undefined, zoneRate?: string): string { + const t = (policy?.RateType ?? '').toLowerCase(); + if (t === 'hour' && typeof policy?.Rate === 'number' && policy.Rate > 0) { + return `$${policy.Rate}/hr`; + } + if (t.includes('variable')) return 'Variable — see signs'; + if (t.includes('free')) return 'Free now'; + if (t.includes('no parking')) return 'No parking now'; + if (t.includes('prepay')) return 'Prepay'; + return fmtRate(zoneRate) ?? '—'; } /** @@ -36,7 +54,10 @@ function policyMeta(p: SpacePolicy): { title: string; rate?: string; color?: str ? '#2e7d32' : undefined; const title = p.DisplayString || p.MessageHeader || p.RateType || 'Rate policy'; - const rate = typeof p.Rate === 'number' && p.Rate > 0 ? `$${p.Rate}/hr` : undefined; + // Only a true "Hour" policy is an hourly price; "Variable" etc. carry a + // placeholder number, so don't render those as $/hr. + const rate = + rt === 'hour' && typeof p.Rate === 'number' && p.Rate > 0 ? `$${p.Rate}/hr` : undefined; return { title, rate, color }; } @@ -56,6 +77,7 @@ export function MeterDetailScreen() { }; const firstSpace = z.Spaces?.[0]; + const currentPolicy = firstSpace?.Policies?.find((p) => p.CurrentSlot); const Field = ({ label, value }: { label: string; value?: string }) => ( @@ -81,7 +103,7 @@ export function MeterDetailScreen() { - +