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>
This commit is contained in:
parent
3837200d89
commit
648acb95cb
2 changed files with 95 additions and 49 deletions
|
|
@ -76,6 +76,7 @@ export function MapScreen() {
|
|||
useEffect(() => {
|
||||
if (mapReady && initialCenter && !positioned.current) {
|
||||
positioned.current = true;
|
||||
console.log('[CAM] initial-positioning ->', JSON.stringify(initialCenter));
|
||||
cameraRef.current?.setCamera?.({
|
||||
centerCoordinate: [initialCenter.longitude, initialCenter.latitude],
|
||||
zoomLevel: initialCenter === DEFAULT_CENTER ? DEFAULT_ZOOM : 14,
|
||||
|
|
@ -108,13 +109,15 @@ export function MapScreen() {
|
|||
const searchThisArea = async () => {
|
||||
try {
|
||||
const center = await mapRef.current?.getCenter?.(); // [lng, lat]
|
||||
console.log('[CAM] searchThisArea getCenter=', JSON.stringify(center));
|
||||
if (center && center.length === 2) {
|
||||
await searchAt({ latitude: center[1], longitude: center[0] }, 'this area');
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
} catch (err) {
|
||||
console.log('[CAM] getCenter threw', String(err));
|
||||
}
|
||||
console.log('[CAM] searchThisArea FALLBACK coords=', JSON.stringify(coords));
|
||||
if (coords) await searchAt(coords, 'this area');
|
||||
};
|
||||
|
||||
|
|
@ -126,6 +129,7 @@ export function MapScreen() {
|
|||
setStatus('No location fix yet — GPS may be unavailable (e.g. indoors).');
|
||||
return;
|
||||
}
|
||||
console.log('[CAM] goToMyLocation setCamera ->', JSON.stringify(c));
|
||||
cameraRef.current?.setCamera?.({
|
||||
centerCoordinate: [c.longitude, c.latitude],
|
||||
zoomLevel: 15,
|
||||
|
|
@ -140,6 +144,7 @@ export function MapScreen() {
|
|||
setStatus('No saved location yet — enable location once to cache it.');
|
||||
return;
|
||||
}
|
||||
console.log('[CAM] searchLastKnown setCamera ->', JSON.stringify(last));
|
||||
cameraRef.current?.setCamera?.({
|
||||
centerCoordinate: [last.longitude, last.latitude],
|
||||
zoomLevel: 15,
|
||||
|
|
@ -180,7 +185,18 @@ export function MapScreen() {
|
|||
style={styles.map}
|
||||
mapStyle={mapStyle}
|
||||
rotateEnabled={false}
|
||||
onDidFinishLoadingMap={() => setMapReady(true)}
|
||||
onDidFinishLoadingMap={() => {
|
||||
console.log('[CAM] onDidFinishLoadingMap (mapReady already', mapReady, ')');
|
||||
setMapReady(true);
|
||||
}}
|
||||
onRegionDidChange={(f: any) =>
|
||||
console.log(
|
||||
'[CAM] regionDidChange center=',
|
||||
JSON.stringify(f?.geometry?.coordinates),
|
||||
'userInteraction=',
|
||||
f?.properties?.isUserInteraction,
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Uncontrolled camera; we position it imperatively (once on load, then
|
||||
only on explicit user actions) so re-renders never move the map. */}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,38 @@ 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);
|
||||
|
|
@ -20,21 +48,32 @@ export function MeterDetailScreen() {
|
|||
|
||||
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={styles.container} contentContainerStyle={{ padding: 16 }}>
|
||||
<Text style={styles.title}>{z.ZoneName ?? 'Parking meter'}</Text>
|
||||
{z.ZoneLocation ? <Text style={styles.sub}>{z.ZoneLocation}</Text> : null}
|
||||
<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={z.Rate != null ? `$${z.Rate}` : undefined} />
|
||||
<Field
|
||||
label="Max time"
|
||||
value={z.MaxTime != null ? `${z.MaxTime} min` : undefined}
|
||||
/>
|
||||
<Field label="Rate" value={fmtRate(z.Rate)} />
|
||||
<Field label="Max time" value={fmtMaxTime(z.MaxTime)} />
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Field
|
||||
|
|
@ -45,27 +84,34 @@ export function MeterDetailScreen() {
|
|||
</View>
|
||||
|
||||
{firstSpace?.Policies?.length ? (
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Rate policies</Text>
|
||||
{firstSpace.Policies.slice(0, 6).map((p, i) => (
|
||||
<Text key={i} style={styles.policy}>
|
||||
• {p.DisplayString ?? p.RateType ?? 'Policy'}
|
||||
{p.Rate != null ? ` — $${p.Rate}` : ''}
|
||||
</Text>
|
||||
))}
|
||||
<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, saved && styles.buttonSaved]}
|
||||
style={[styles.button, { backgroundColor: saved ? '#2e7d32' : colors.card }]}
|
||||
onPress={onSave}
|
||||
disabled={saved}
|
||||
>
|
||||
<Text style={styles.buttonText}>{saved ? 'Saved ✓' : 'Save kiosk'}</Text>
|
||||
<Text style={[styles.buttonText, { color: saved ? '#fff' : colors.text }]}>
|
||||
{saved ? 'Saved ✓' : 'Save kiosk'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.buttonPrimary]}
|
||||
style={[styles.button, { backgroundColor: colors.primary }]}
|
||||
onPress={() =>
|
||||
Alert.alert(
|
||||
'Start session',
|
||||
|
|
@ -73,40 +119,24 @@ export function MeterDetailScreen() {
|
|||
)
|
||||
}
|
||||
>
|
||||
<Text style={styles.buttonText}>Start parking session</Text>
|
||||
<Text style={[styles.buttonText, { color: '#fff' }]}>Start parking session</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, value }: { label: string; value?: string }) {
|
||||
return (
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.fieldLabel}>{label}</Text>
|
||||
<Text style={styles.fieldValue}>{value ?? '—'}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
title: { fontSize: 24, fontWeight: '700' },
|
||||
sub: { color: '#666', marginBottom: 12 },
|
||||
sub: { marginBottom: 12 },
|
||||
row: { flexDirection: 'row', gap: 12, marginTop: 8 },
|
||||
field: { flex: 1, backgroundColor: '#f2f2f2', borderRadius: 10, padding: 12 },
|
||||
fieldLabel: { fontSize: 12, color: '#888' },
|
||||
field: { flex: 1, borderRadius: 10, padding: 12 },
|
||||
fieldLabel: { fontSize: 12 },
|
||||
fieldValue: { fontSize: 16, fontWeight: '600', marginTop: 2 },
|
||||
card: { backgroundColor: '#f7f7f7', borderRadius: 10, padding: 14, marginTop: 16 },
|
||||
cardTitle: { fontWeight: '700', marginBottom: 6 },
|
||||
policy: { color: '#444', marginBottom: 2 },
|
||||
button: {
|
||||
marginTop: 16,
|
||||
backgroundColor: '#555',
|
||||
borderRadius: 10,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonPrimary: { backgroundColor: '#1e6f5c' },
|
||||
buttonSaved: { backgroundColor: '#2e7d32' },
|
||||
buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 },
|
||||
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 },
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue