UI improvements and prepare for OSM migration
This commit is contained in:
+40
@@ -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
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
Reference in New Issue
Block a user