BigBrainParking/app/src/screens/MeterDetailScreen.tsx
Hank 648acb95cb Meter screen: theme it + fix rate/max-time/policy display; add camera trace logs
- MeterDetail now themed (fixes unreadable black headings in dark mode)
- Rate no longer double-'$'; MaxTime 0 -> "Unlimited"; policies show a clean
  title + time window and hide sentinel (<=0) rates
- Temporary [CAM] logging on the map to trace the persistent recenter-on-search

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:05:42 -07:00

142 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import type { RouteProp } from '@react-navigation/native';
import { useRoute } from '@react-navigation/native';
import type { RootStackParamList } from '@/navigation/RootNavigator';
import { saveKiosk, toSavedKiosk } from '@/features/favorites/favoritesStore';
import { useTheme } from '@/theme/ThemeContext';
import type { SpacePolicy } from 'parksmarter-client';
type DetailRoute = RouteProp<RootStackParamList, 'MeterDetail'>;
/** MaxTime of 0 means no cap. */
function fmtMaxTime(m?: number): string {
if (m == null) return '—';
if (m <= 0) return 'Unlimited';
return `${m} min`;
}
/** Zone rate comes as a string that sometimes already includes a '$'. */
function fmtRate(r?: string): string | undefined {
if (r == null) return undefined;
return '$' + String(r).replace(/^\$+/, '');
}
function policyLine(p: SpacePolicy): { title: string; sub: string } {
const title = p.DisplayString || p.MessageHeader || p.RateType || 'Rate policy';
const parts: string[] = [];
if (p.StartTimeDisplay && p.EndTimeDisplay) {
parts.push(`${p.StartTimeDisplay}${p.EndTimeDisplay}`);
}
// Negative/zero rates are sentinels ("no charge" / "n/a") — only show real ones.
if (typeof p.Rate === 'number' && p.Rate > 0) parts.push(`$${p.Rate}/hr`);
if (p.MaxTime != null) parts.push(`${fmtMaxTime(p.MaxTime)} max`);
return { title, sub: parts.join(' · ') };
}
export function MeterDetailScreen() {
const { colors } = useTheme();
const { params } = useRoute<DetailRoute>();
const z = params.zone;
const [saved, setSaved] = useState(false);
const onSave = async () => {
await saveKiosk(toSavedKiosk(z));
setSaved(true);
Alert.alert('Saved', `${z.ZoneName ?? 'Kiosk'} added to your saved kiosks.`);
};
const firstSpace = z.Spaces?.[0];
const Field = ({ label, value }: { label: string; value?: string }) => (
<View style={[styles.field, { backgroundColor: colors.card }]}>
<Text style={[styles.fieldLabel, { color: colors.subtext }]}>{label}</Text>
<Text style={[styles.fieldValue, { color: colors.text }]}>{value ?? '—'}</Text>
</View>
);
return (
<ScrollView
style={{ backgroundColor: colors.bg }}
contentContainerStyle={{ padding: 16 }}
>
<Text style={[styles.title, { color: colors.text }]}>
{z.ZoneName ?? 'Parking meter'}
</Text>
{z.ZoneLocation ? (
<Text style={[styles.sub, { color: colors.subtext }]}>{z.ZoneLocation}</Text>
) : null}
<View style={styles.row}>
<Field label="Scanner code" value={z.ScannerCode} />
<Field label="Serial" value={z.TerminalSerNo} />
</View>
<View style={styles.row}>
<Field label="Rate" value={fmtRate(z.Rate)} />
<Field label="Max time" value={fmtMaxTime(z.MaxTime)} />
</View>
<View style={styles.row}>
<Field
label="Occupancy"
value={z.PercentageFull != null ? `${z.PercentageFull}% full` : undefined}
/>
<Field label="Spaces" value={z.Spaces ? String(z.Spaces.length) : undefined} />
</View>
{firstSpace?.Policies?.length ? (
<View style={[styles.card, { backgroundColor: colors.card }]}>
<Text style={[styles.cardTitle, { color: colors.text }]}>Rate policies</Text>
{firstSpace.Policies.slice(0, 8).map((p, i) => {
const { title, sub } = policyLine(p);
return (
<View key={i} style={styles.policyRow}>
<Text style={[styles.policyTitle, { color: colors.text }]}>{title}</Text>
{sub ? (
<Text style={[styles.policySub, { color: colors.subtext }]}>{sub}</Text>
) : null}
</View>
);
})}
</View>
) : null}
<TouchableOpacity
style={[styles.button, { backgroundColor: saved ? '#2e7d32' : colors.card }]}
onPress={onSave}
disabled={saved}
>
<Text style={[styles.buttonText, { color: saved ? '#fff' : colors.text }]}>
{saved ? 'Saved ✓' : 'Save kiosk'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, { backgroundColor: colors.primary }]}
onPress={() =>
Alert.alert(
'Start session',
'Starting a paid session is a real charge — this flow (vehicle + duration + card selection) is wired to postStartParkingSession and will be enabled after review.',
)
}
>
<Text style={[styles.buttonText, { color: '#fff' }]}>Start parking session</Text>
</TouchableOpacity>
</ScrollView>
);
}
const styles = StyleSheet.create({
title: { fontSize: 24, fontWeight: '700' },
sub: { marginBottom: 12 },
row: { flexDirection: 'row', gap: 12, marginTop: 8 },
field: { flex: 1, borderRadius: 10, padding: 12 },
fieldLabel: { fontSize: 12 },
fieldValue: { fontSize: 16, fontWeight: '600', marginTop: 2 },
card: { borderRadius: 10, padding: 14, marginTop: 16 },
cardTitle: { fontWeight: '700', marginBottom: 8, fontSize: 15 },
policyRow: { marginBottom: 8 },
policyTitle: { fontSize: 14, fontWeight: '600' },
policySub: { fontSize: 12, marginTop: 1 },
button: { marginTop: 16, borderRadius: 10, padding: 16, alignItems: 'center' },
buttonText: { fontWeight: '600', fontSize: 16 },
});