UI improvements and prepare for OSM migration

This commit is contained in:
2026-07-28 20:05:12 +02:00
parent 6ac78ea6a1
commit c6240784ff
6 changed files with 146 additions and 98 deletions
@@ -0,0 +1,40 @@
# Mover entorno Android y Gradle a C:\Android
Para solucionar definitivamente el error `[CXX1429]` causado por la letra "ñ" en la ruta del usuario, moveremos el SDK de Android y la caché de Gradle a una carpeta raíz sin caracteres especiales.
## User Review Required
> [!CAUTION]
> Voy a mover archivos del SDK de Android (varios GB). Esto puede tardar unos minutos. Una vez terminado, deberás actualizar la ruta del SDK en los ajustes de Android Studio a `C:\Android\sdk`.
## Proposed Changes
### Preparación de Directorios
- Crear `C:\Android`
- Crear `C:\Android\sdk` (Nuevo destino del Android SDK)
- Crear `C:\Android\gradle` (Nuevo destino de la caché de Gradle)
- Crear `C:\Android\home` (Nuevo destino para configuraciones de Android)
### Migración de Archivos
- Mover el contenido de `C:\Users\JavierBraña\AppData\Local\Android\Sdk` a `C:\Android\sdk`.
### Configuración del Proyecto
#### [MODIFY] [gradlew.bat](file:///C:/xampp/htdocs/Avante%20movil/android/gradlew.bat)
Actualizar el script para que use las nuevas rutas en cada ejecución:
- `GRADLE_USER_HOME=C:\Android\gradle`
- `ANDROID_SDK_ROOT=C:\Android\sdk`
- `ANDROID_USER_HOME=C:\Android\home`
- `-Duser.home=C:\Android\home`
## Verification Plan
### Automated Tests
- Ejecutar `./gradlew :app:assembleDebug`.
- Verificar que las rutas en el log de Prefab apunten a `C:\Android\gradle`.
### Manual Verification
- Comprobar que no aparezca el error `ClassNotFoundException: com.google.prefab.cli.AppKt`.
- Comprobar que el servicio `AndroidLocationsBuildService` se inicie correctamente.
@@ -1,4 +1,5 @@
- [ ] Verify short path for user directory - [x] Crear estructura de directorios en C:\Android
- [ ] Test build with relocated Gradle User Home - [x] Mover el Android SDK a C:\Android\sdk
- [ ] Configure project or environment for permanent fix - [x] Actualizar gradlew.bat con las nuevas rutas
- [ ] Final verification of app build - [/] Ejecutar build de limpieza y ensamblado
- [ ] Verificación final
@@ -0,0 +1,12 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
toolchainVersion=23
-9
View File
@@ -235,15 +235,6 @@ export function InspectionFormScreen({ route, navigation }: Props) {
<Text style={styles.tplChevron}></Text> <Text style={styles.tplChevron}></Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
<View style={{ height: 8 }} />
<PrimaryButton
title="Inspección libre (sin plantilla)"
variant="ghost"
onPress={() => pickTemplate(null)}
/>
<View style={{ height: 8 }} />
<PrimaryButton title="Cancelar" variant="ghost" onPress={() => navigation.goBack()} />
</ScrollView> </ScrollView>
); );
} }
+52 -49
View File
@@ -1,5 +1,5 @@
import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { NativeStackScreenProps } from '@react-navigation/native-stack';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
@@ -16,7 +16,7 @@ import { getProjects, saveProjectList, saveTemplates, setActiveProjectId } from
import { isOnline } from '../net/connectivity'; import { isOnline } from '../net/connectivity';
import { runSync } from '../sync/engine'; import { runSync } from '../sync/engine';
import { RootStackParamList } from '../navigation/types'; import { RootStackParamList } from '../navigation/types';
import { COLORS, PrimaryButton } from '../ui/components'; import { COLORS } from '../ui/components';
type Props = NativeStackScreenProps<RootStackParamList, 'Projects'>; type Props = NativeStackScreenProps<RootStackParamList, 'Projects'>;
@@ -33,7 +33,26 @@ export function ProjectsScreen({ navigation }: Props) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [opening, setOpening] = useState<number | null>(null); const [opening, setOpening] = useState<number | null>(null);
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const dropdownRef = useRef<View>(null);
// Mueve el menú de usuario al header nativo
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<View style={styles.headerRight}>
<TouchableOpacity
onPress={() => setShowMenu(!showMenu)}
activeOpacity={0.7}
style={styles.userButton}
>
<Text style={styles.dropdownLabel}>
{user?.name?.split(' ')[0] ?? 'Usuario'}
<Text style={styles.chevronSmall}> </Text>
</Text>
</TouchableOpacity>
</View>
),
});
}, [navigation, user, showMenu]);
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -41,9 +60,6 @@ export function ProjectsScreen({ navigation }: Props) {
if (await isOnline()) { if (await isOnline()) {
const list = normalize(await api.listProjects()); const list = normalize(await api.listProjects());
await saveProjectList(list); await saveProjectList(list);
// Prefetch del catálogo global de plantillas: así quedan SIEMPRE en
// local y se puede inspeccionar offline aunque el proyecto no se haya
// abierto todavía. No bloquea la lista si /templates falla.
try { try {
const { templates } = await api.getTemplates(); const { templates } = await api.getTemplates();
await saveTemplates(templates); await saveTemplates(templates);
@@ -69,8 +85,6 @@ export function ProjectsScreen({ navigation }: Props) {
setOpening(p.id); setOpening(p.id);
try { try {
await setActiveProjectId(p.id); await setActiveProjectId(p.id);
// runSync ya resuelve ambos casos: sin cursor baja el snapshot completo,
// con cursor baja el delta (y vacía primero el outbox, vacío en la 1ª vez).
if (await isOnline()) await runSync(p.id); if (await isOnline()) await runSync(p.id);
navigation.navigate('ProjectDetail', { projectId: p.id, name: p.name }); navigation.navigate('ProjectDetail', { projectId: p.id, name: p.name });
} finally { } finally {
@@ -92,21 +106,9 @@ export function ProjectsScreen({ navigation }: Props) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
{/* Header with dropdown menu */} {/* Menu flotante (se posiciona relativo al contenedor principal) */}
<View style={styles.header}>
<Text style={styles.headerTitle}>Proyectos</Text>
<TouchableOpacity
ref={dropdownRef}
style={styles.dropdownWrapper}
onPress={() => setShowMenu(!showMenu)}
activeOpacity={1}
>
<Text style={styles.dropdownLabel}>
{user?.name ?? 'Usuario'}
<Text style={styles.chevron}> </Text>
</Text>
{showMenu && ( {showMenu && (
<View style={styles.dropdownMenu} pointerEvents="box-none"> <View style={styles.floatingMenu}>
<TouchableOpacity style={styles.dropdownItem} onPress={handleSettings} activeOpacity={0.7}> <TouchableOpacity style={styles.dropdownItem} onPress={handleSettings} activeOpacity={0.7}>
<Text style={styles.dropdownItemText}> Configuración</Text> <Text style={styles.dropdownItemText}> Configuración</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -115,11 +117,13 @@ export function ProjectsScreen({ navigation }: Props) {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
</TouchableOpacity>
</View>
{/* Close dropdown when tapping outside */} {/* Close dropdown when tapping outside */}
<TouchableOpacity onPress={() => setShowMenu(false)} style={styles.touchOutside}> <TouchableOpacity
onPress={() => setShowMenu(false)}
style={styles.touchOutside}
activeOpacity={1}
>
<FlatList <FlatList
data={projects} data={projects}
keyExtractor={(p) => String(p.id)} keyExtractor={(p) => String(p.id)}
@@ -147,43 +151,40 @@ export function ProjectsScreen({ navigation }: Props) {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1, backgroundColor: '#fff' },
header: { headerRight: {
flexDirection: 'row', marginRight: 8,
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
}, },
headerTitle: { fontSize: 20, fontWeight: '700', color: COLORS.primary }, userButton: {
dropdownWrapper: { paddingHorizontal: 8,
position: 'relative', paddingVertical: 4,
}, },
dropdownLabel: { dropdownLabel: {
fontSize: 16, fontSize: 15,
fontWeight: '600', fontWeight: '600',
color: '#333', color: COLORS.primary,
}, },
chevron: { fontSize: 14, color: '#666' }, chevronSmall: { fontSize: 10, color: COLORS.primary },
dropdownMenu: { floatingMenu: {
position: 'absolute', position: 'absolute',
top: 40, top: 4,
right: 0, right: 16,
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: 8, borderRadius: 8,
borderWidth: 1, borderWidth: 1,
borderColor: COLORS.border, borderColor: COLORS.border,
overflow: 'hidden', overflow: 'hidden',
minWidth: 180, minWidth: 180,
elevation: 4, elevation: 10,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1, shadowOpacity: 0.2,
shadowRadius: 4, shadowRadius: 5,
zIndex: 100, zIndex: 1000,
}, },
dropdownItem: { dropdownItem: {
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 14,
}, },
dropdownItemDanger: { dropdownItemDanger: {
borderTopWidth: StyleSheet.hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
@@ -201,13 +202,15 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 16, paddingVertical: 18,
borderTopWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLORS.border, borderColor: COLORS.border,
backgroundColor: '#fff', backgroundColor: '#fff',
}, },
cardContent: { flex: 1 }, cardContent: { flex: 1 },
cardName: { fontSize: 16, fontWeight: '600', color: '#111' }, cardName: { fontSize: 16, fontWeight: '700', color: '#111' },
cardMeta: { fontSize: 13, color: COLORS.muted, marginTop: 4 }, cardMeta: { fontSize: 13, color: COLORS.muted, marginTop: 4 },
chevron: { fontSize: 20, color: COLORS.border, marginLeft: 8 },
empty: { textAlign: 'center', marginTop: 40, color: '#888' }, empty: { textAlign: 'center', marginTop: 40, color: '#888' },
}); });
+28 -27
View File
@@ -26,6 +26,34 @@ export function FeatureDetailContent({ featureId }: { featureId: number }) {
const [inspections, setInspections] = useState<Inspection[]>([]); const [inspections, setInspections] = useState<Inspection[]>([]);
const [featureTypes, setFeatureTypes] = useState<FeatureType[]>([]); const [featureTypes, setFeatureTypes] = useState<FeatureType[]>([]);
// Header right button for Nueva inspección
// MOVIDO AQUÍ: Los hooks deben ir siempre antes de cualquier return.
useFocusEffect(
useCallback(() => {
if (canInspect && feature) {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity
style={styles.headerButton}
onPress={() =>
navigation.navigate('InspectionForm', {
featureId: feature.id,
featureName: feature.name,
templateId: feature.template_id ?? undefined,
})
}
>
<Text style={styles.headerButtonText}>+ Inspección</Text>
</TouchableOpacity>
),
});
}
return () => {
navigation.setOptions({ headerRight: undefined });
};
}, [navigation, canInspect, feature]),
);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const [f, ins, types] = await Promise.all([ const [f, ins, types] = await Promise.all([
getFeature(featureId), getFeature(featureId),
@@ -52,33 +80,6 @@ export function FeatureDetailContent({ featureId }: { featureId: number }) {
const featureType = featureTypes.find((t) => t.id === feature.feature_type_id); const featureType = featureTypes.find((t) => t.id === feature.feature_type_id);
const isActive = feature.is_active == null || Number(feature.is_active) !== 0; const isActive = feature.is_active == null || Number(feature.is_active) !== 0;
// Header right button for Nueva inspección
useFocusEffect(
useCallback(() => {
if (canInspect) {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity
style={styles.headerButton}
onPress={() =>
navigation.navigate('InspectionForm', {
featureId: feature.id,
featureName: feature.name,
templateId: feature.template_id ?? undefined,
})
}
>
<Text style={styles.headerButtonText}>+ Inspección</Text>
</TouchableOpacity>
),
});
}
return () => {
navigation.setOptions({ headerRight: undefined });
};
}, [navigation, canInspect, feature?.id, feature?.name, feature?.template_id]),
);
return ( return (
<ScrollView contentContainerStyle={styles.body}> <ScrollView contentContainerStyle={styles.body}>
<View style={styles.badges}> <View style={styles.badges}>