Fixed mobile form
This commit is contained in:
@@ -44,6 +44,189 @@ import { useDataSync } from './core/hooks/useDataSync';
|
||||
import { MobileDebugPanel } from './core/debug/MobileDebugPanel';
|
||||
import { MobileErrorBoundary } from './core/error-boundaries/MobileErrorBoundary';
|
||||
|
||||
// Hoisted mobile screen components to stabilize identity and prevent remounts
|
||||
const DashboardScreen: React.FC = () => (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-lg font-semibold text-slate-800 mb-2">Dashboard</h2>
|
||||
<p className="text-slate-500">Coming soon - Vehicle insights and analytics</p>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LogFuelScreen: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
||||
const { goBack, canGoBack, navigateToScreen } = useNavigationStore();
|
||||
useEffect(() => {
|
||||
console.log('[LogFuelScreen] Mounted');
|
||||
return () => console.log('[LogFuelScreen] Unmounted');
|
||||
}, []);
|
||||
|
||||
let fuelLogs: FuelLogResponse[] | undefined, isLoading: boolean | undefined, error: any;
|
||||
try {
|
||||
const hookResult = useFuelLogs();
|
||||
fuelLogs = hookResult.fuelLogs;
|
||||
isLoading = hookResult.isLoading;
|
||||
error = hookResult.error;
|
||||
} catch (hookError) {
|
||||
console.error('[LogFuelScreen] Hook error:', hookError);
|
||||
error = hookError;
|
||||
}
|
||||
|
||||
const handleEdit = (log: FuelLogResponse) => {
|
||||
if (!log || !log.id) {
|
||||
console.error('[LogFuelScreen] Invalid log data for edit:', log);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setEditingLog(log);
|
||||
} catch (error) {
|
||||
console.error('[LogFuelScreen] Error setting editing log:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (_logId: string) => {
|
||||
try {
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh fuel logs after delete:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEdit = async (id: string, data: UpdateFuelLogRequest) => {
|
||||
try {
|
||||
await fuelLogsApi.update(id, data);
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
|
||||
setEditingLog(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to update fuel log:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseEdit = () => setEditingLog(null);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-600 mb-4">Failed to load fuel logs</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading === undefined) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
Initializing fuel logs...
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<MobileErrorBoundary screenName="FuelLogForm" key="fuel-form">
|
||||
<FuelLogForm onSuccess={() => {
|
||||
// Refresh dependent data
|
||||
try {
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
|
||||
} catch {}
|
||||
// Navigate back if we have history; otherwise go to Vehicles
|
||||
if (canGoBack()) {
|
||||
goBack();
|
||||
} else {
|
||||
navigateToScreen('Vehicles', { source: 'fuel-log-added' });
|
||||
}
|
||||
}} />
|
||||
</MobileErrorBoundary>
|
||||
<MobileErrorBoundary screenName="FuelLogsSection" key="fuel-section">
|
||||
<GlassCard>
|
||||
<div className="py-2">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
Loading fuel logs...
|
||||
</div>
|
||||
) : (
|
||||
<FuelLogsList
|
||||
logs={fuelLogs || []}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</MobileErrorBoundary>
|
||||
|
||||
<MobileErrorBoundary screenName="FuelLogEditDialog" key="fuel-edit-dialog">
|
||||
<FuelLogEditDialog
|
||||
open={!!editingLog}
|
||||
log={editingLog}
|
||||
onClose={handleCloseEdit}
|
||||
onSave={handleSaveEdit}
|
||||
/>
|
||||
</MobileErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AddVehicleScreenProps {
|
||||
onBack: () => void;
|
||||
onAdded: () => void;
|
||||
}
|
||||
|
||||
const AddVehicleScreen: React.FC<AddVehicleScreenProps> = ({ onBack, onAdded }) => {
|
||||
const { optimisticCreateVehicle } = useOptimisticVehicles([]);
|
||||
|
||||
const handleCreateVehicle = async (data: CreateVehicleRequest) => {
|
||||
try {
|
||||
await optimisticCreateVehicle(data);
|
||||
onAdded();
|
||||
} catch (error) {
|
||||
console.error('Failed to create vehicle:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-slate-800">Add Vehicle</h2>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<VehicleForm
|
||||
onSubmit={handleCreateVehicle}
|
||||
onCancel={onBack}
|
||||
/>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
const { isLoading, isAuthenticated, loginWithRedirect, user } = useAuth0();
|
||||
@@ -152,184 +335,9 @@ function App() {
|
||||
<MobileDebugPanel visible={import.meta.env.MODE === 'development'} />
|
||||
);
|
||||
|
||||
// Placeholder screens for mobile
|
||||
const DashboardScreen = () => (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-lg font-semibold text-slate-800 mb-2">Dashboard</h2>
|
||||
<p className="text-slate-500">Coming soon - Vehicle insights and analytics</p>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LogFuelScreen = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
||||
|
||||
// Safe hook usage with error boundary protection
|
||||
let fuelLogs, isLoading, error;
|
||||
|
||||
try {
|
||||
const hookResult = useFuelLogs();
|
||||
fuelLogs = hookResult.fuelLogs;
|
||||
isLoading = hookResult.isLoading;
|
||||
error = hookResult.error;
|
||||
} catch (hookError) {
|
||||
console.error('[LogFuelScreen] Hook error:', hookError);
|
||||
error = hookError;
|
||||
}
|
||||
|
||||
const handleEdit = (log: FuelLogResponse) => {
|
||||
// Defensive validation before setting editing log
|
||||
if (!log || !log.id) {
|
||||
console.error('[LogFuelScreen] Invalid log data for edit:', log);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setEditingLog(log);
|
||||
} catch (error) {
|
||||
console.error('[LogFuelScreen] Error setting editing log:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (_logId: string) => {
|
||||
try {
|
||||
// Invalidate queries to refresh the data
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh fuel logs after delete:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEdit = async (id: string, data: UpdateFuelLogRequest) => {
|
||||
try {
|
||||
await fuelLogsApi.update(id, data);
|
||||
// Invalidate queries to refresh the data
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
|
||||
setEditingLog(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to update fuel log:', error);
|
||||
throw error; // Re-throw to let the dialog handle the error
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseEdit = () => {
|
||||
setEditingLog(null);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-600 mb-4">Failed to load fuel logs</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Add loading state for component initialization
|
||||
if (isLoading === undefined) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
Initializing fuel logs...
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<MobileErrorBoundary screenName="FuelLogForm" key="fuel-form">
|
||||
<FuelLogForm />
|
||||
</MobileErrorBoundary>
|
||||
<MobileErrorBoundary screenName="FuelLogsSection" key="fuel-section">
|
||||
<GlassCard>
|
||||
<div className="py-2">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
Loading fuel logs...
|
||||
</div>
|
||||
) : (
|
||||
<FuelLogsList
|
||||
logs={fuelLogs || []}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</MobileErrorBoundary>
|
||||
|
||||
{/* Edit Dialog */}
|
||||
<MobileErrorBoundary screenName="FuelLogEditDialog" key="fuel-edit-dialog">
|
||||
<FuelLogEditDialog
|
||||
open={!!editingLog}
|
||||
log={editingLog}
|
||||
onClose={handleCloseEdit}
|
||||
onSave={handleSaveEdit}
|
||||
/>
|
||||
</MobileErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Mobile settings now uses the dedicated MobileSettingsScreen component
|
||||
const SettingsScreen = MobileSettingsScreen;
|
||||
|
||||
const AddVehicleScreen = () => {
|
||||
// Vehicle creation logic
|
||||
const { optimisticCreateVehicle } = useOptimisticVehicles([]);
|
||||
|
||||
const handleCreateVehicle = async (data: CreateVehicleRequest) => {
|
||||
try {
|
||||
await optimisticCreateVehicle(data);
|
||||
// Success - navigate back to list
|
||||
handleVehicleAdded();
|
||||
} catch (error) {
|
||||
console.error('Failed to create vehicle:', error);
|
||||
// Error handling is done by the useOptimisticVehicles hook via toast
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlassCard>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-slate-800">Add Vehicle</h2>
|
||||
<button
|
||||
onClick={handleBackToList}
|
||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<VehicleForm
|
||||
onSubmit={handleCreateVehicle}
|
||||
onCancel={handleBackToList}
|
||||
/>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
if (mobileMode) {
|
||||
return (
|
||||
@@ -425,7 +433,7 @@ function App() {
|
||||
>
|
||||
<MobileErrorBoundary screenName="Vehicles">
|
||||
{vehicleSubScreen === 'add' || showAddVehicle ? (
|
||||
<AddVehicleScreen />
|
||||
<AddVehicleScreen onBack={handleBackToList} onAdded={handleVehicleAdded} />
|
||||
) : selectedVehicle && (vehicleSubScreen === 'detail') ? (
|
||||
<VehicleDetailMobile
|
||||
vehicle={selectedVehicle}
|
||||
|
||||
@@ -115,6 +115,12 @@ export class DataSyncManager {
|
||||
private startBackgroundSync() {
|
||||
this.syncInterval = setInterval(() => {
|
||||
if (this.isOnline) {
|
||||
const state = useNavigationStore.getState();
|
||||
console.log('[DataSync] Tick', {
|
||||
at: new Date().toISOString(),
|
||||
activeScreen: state.activeScreen,
|
||||
selectedVehicleId: state.selectedVehicleId,
|
||||
});
|
||||
this.performBackgroundSync();
|
||||
}
|
||||
}, this.config.syncInterval);
|
||||
@@ -135,14 +141,19 @@ export class DataSyncManager {
|
||||
|
||||
// If on vehicles screen, refresh vehicles data
|
||||
if (navigationState.activeScreen === 'Vehicles') {
|
||||
console.log('[DataSync] Invalidating query', { key: ['vehicles'], reason: 'background-sync:vehicles-screen' });
|
||||
await this.queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||
}
|
||||
|
||||
// If viewing specific vehicle, refresh its data
|
||||
if (navigationState.selectedVehicleId) {
|
||||
await this.queryClient.invalidateQueries({
|
||||
queryKey: ['vehicles', navigationState.selectedVehicleId]
|
||||
});
|
||||
// If viewing specific vehicle details, refresh that vehicle's data
|
||||
if (
|
||||
navigationState.activeScreen === 'Vehicles' &&
|
||||
navigationState.vehicleSubScreen === 'detail' &&
|
||||
navigationState.selectedVehicleId
|
||||
) {
|
||||
const key = ['vehicles', navigationState.selectedVehicleId];
|
||||
console.log('[DataSync] Invalidating query', { key, reason: 'background-sync:selected-vehicle:vehicles-detail' });
|
||||
await this.queryClient.invalidateQueries({ queryKey: key });
|
||||
}
|
||||
|
||||
console.log('DataSync: Background sync completed');
|
||||
@@ -241,4 +252,4 @@ export class DataSyncManager {
|
||||
window.removeEventListener('online', this.handleOnline);
|
||||
window.removeEventListener('offline', this.handleOffline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
||||
const [useOdometer, setUseOdometer] = useState(false);
|
||||
const formInitialized = useRef(false);
|
||||
|
||||
const { control, handleSubmit, watch, setValue, formState: { errors, isValid } } = useForm<CreateFuelLogRequest>({
|
||||
const { control, handleSubmit, watch, setValue, reset, formState: { errors, isValid } } = useForm<CreateFuelLogRequest>({
|
||||
resolver: zodResolver(schema),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
@@ -47,14 +47,22 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
||||
});
|
||||
|
||||
// DEBUG: Log component renders and form state
|
||||
console.log('[FuelLogForm] Render - formInitialized:', formInitialized.current, 'isLoading:', isLoading);
|
||||
console.log('[FuelLogForm] Render', {
|
||||
at: new Date().toISOString(),
|
||||
formInitialized: formInitialized.current,
|
||||
isLoading,
|
||||
});
|
||||
|
||||
// Prevent form reset after initial load
|
||||
useEffect(() => {
|
||||
console.log('[FuelLogForm] Mounted');
|
||||
if (!formInitialized.current) {
|
||||
formInitialized.current = true;
|
||||
console.log('[FuelLogForm] Form initialized');
|
||||
}
|
||||
return () => {
|
||||
console.log('[FuelLogForm] Unmounted');
|
||||
};
|
||||
}, []);
|
||||
|
||||
// DEBUG: Watch for form value changes
|
||||
@@ -63,6 +71,16 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
||||
console.log('[FuelLogForm] Vehicle ID changed:', vehicleId);
|
||||
}, [vehicleId]);
|
||||
|
||||
// DEBUG: Track dirty-state changes to detect resets
|
||||
useEffect(() => {
|
||||
const subscription = watch((_, info) => {
|
||||
if (info.name) {
|
||||
console.log('[FuelLogForm] Field change', { name: info.name, type: info.type });
|
||||
}
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [watch]);
|
||||
|
||||
const watched = watch(['fuelUnits', 'costPerUnit']);
|
||||
const [fuelUnitsRaw, costPerUnitRaw] = watched as [string | number | undefined, string | number | undefined];
|
||||
|
||||
@@ -83,6 +101,19 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
||||
tripDistance: useOdometer ? undefined : data.tripDistance,
|
||||
};
|
||||
await createFuelLog(payload);
|
||||
// Reset form to initial defaults after successful create
|
||||
reset({
|
||||
vehicleId: undefined as any,
|
||||
dateTime: new Date().toISOString().slice(0, 16),
|
||||
odometerReading: undefined as any,
|
||||
tripDistance: undefined as any,
|
||||
fuelType: FuelType.GASOLINE,
|
||||
fuelGrade: undefined as any,
|
||||
fuelUnits: undefined as any,
|
||||
costPerUnit: undefined as any,
|
||||
locationData: undefined as any,
|
||||
notes: undefined as any,
|
||||
} as any);
|
||||
onSuccess?.();
|
||||
};
|
||||
|
||||
@@ -178,4 +209,3 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
||||
};
|
||||
|
||||
export const FuelLogForm = memo(FuelLogFormComponent);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user