diff --git a/android/.artifacts/8b46590a-f8f3-4d4b-a25e-e9e71ce04086/implementation_plan.artifact.md b/android/.artifacts/8b46590a-f8f3-4d4b-a25e-e9e71ce04086/implementation_plan.artifact.md
new file mode 100644
index 0000000..723e236
--- /dev/null
+++ b/android/.artifacts/8b46590a-f8f3-4d4b-a25e-e9e71ce04086/implementation_plan.artifact.md
@@ -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.
diff --git a/android/.artifacts/8b46590a-f8f3-4d4b-a25e-e9e71ce04086/task.artifact.md b/android/.artifacts/8b46590a-f8f3-4d4b-a25e-e9e71ce04086/task.artifact.md
index 1b6b69c..39f6ffc 100644
--- a/android/.artifacts/8b46590a-f8f3-4d4b-a25e-e9e71ce04086/task.artifact.md
+++ b/android/.artifacts/8b46590a-f8f3-4d4b-a25e-e9e71ce04086/task.artifact.md
@@ -1,4 +1,5 @@
-- [ ] Verify short path for user directory
-- [ ] Test build with relocated Gradle User Home
-- [ ] Configure project or environment for permanent fix
-- [ ] Final verification of app build
+- [x] Crear estructura de directorios en C:\Android
+- [x] Mover el Android SDK a C:\Android\sdk
+- [x] Actualizar gradlew.bat con las nuevas rutas
+- [/] Ejecutar build de limpieza y ensamblado
+- [ ] Verificación final
diff --git a/android/gradle/gradle-daemon-jvm.properties b/android/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 0000000..15dd3ba
--- /dev/null
+++ b/android/gradle/gradle-daemon-jvm.properties
@@ -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
diff --git a/src/screens/InspectionFormScreen.tsx b/src/screens/InspectionFormScreen.tsx
index 7257dfd..4778988 100644
--- a/src/screens/InspectionFormScreen.tsx
+++ b/src/screens/InspectionFormScreen.tsx
@@ -235,15 +235,6 @@ export function InspectionFormScreen({ route, navigation }: Props) {
›
))}
-
-
- pickTemplate(null)}
- />
-
- navigation.goBack()} />
);
}
diff --git a/src/screens/ProjectsScreen.tsx b/src/screens/ProjectsScreen.tsx
index 1bc9612..5b334b2 100644
--- a/src/screens/ProjectsScreen.tsx
+++ b/src/screens/ProjectsScreen.tsx
@@ -1,5 +1,5 @@
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 {
ActivityIndicator,
FlatList,
@@ -16,7 +16,7 @@ import { getProjects, saveProjectList, saveTemplates, setActiveProjectId } from
import { isOnline } from '../net/connectivity';
import { runSync } from '../sync/engine';
import { RootStackParamList } from '../navigation/types';
-import { COLORS, PrimaryButton } from '../ui/components';
+import { COLORS } from '../ui/components';
type Props = NativeStackScreenProps;
@@ -33,7 +33,26 @@ export function ProjectsScreen({ navigation }: Props) {
const [loading, setLoading] = useState(false);
const [opening, setOpening] = useState(null);
const [showMenu, setShowMenu] = useState(false);
- const dropdownRef = useRef(null);
+
+ // Mueve el menú de usuario al header nativo
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ headerRight: () => (
+
+ setShowMenu(!showMenu)}
+ activeOpacity={0.7}
+ style={styles.userButton}
+ >
+
+ {user?.name?.split(' ')[0] ?? 'Usuario'}
+ ▼
+
+
+
+ ),
+ });
+ }, [navigation, user, showMenu]);
const load = useCallback(async () => {
setLoading(true);
@@ -41,9 +60,6 @@ export function ProjectsScreen({ navigation }: Props) {
if (await isOnline()) {
const list = normalize(await api.listProjects());
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 {
const { templates } = await api.getTemplates();
await saveTemplates(templates);
@@ -69,8 +85,6 @@ export function ProjectsScreen({ navigation }: Props) {
setOpening(p.id);
try {
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);
navigation.navigate('ProjectDetail', { projectId: p.id, name: p.name });
} finally {
@@ -92,34 +106,24 @@ export function ProjectsScreen({ navigation }: Props) {
return (
- {/* Header with dropdown menu */}
-
- Proyectos
- setShowMenu(!showMenu)}
- activeOpacity={1}
- >
-
- {user?.name ?? 'Usuario'}
- ▼
-
- {showMenu && (
-
-
- ⚙ Configuración
-
-
- Salir
-
-
- )}
-
-
+ {/* Menu flotante (se posiciona relativo al contenedor principal) */}
+ {showMenu && (
+
+
+ ⚙ Configuración
+
+
+ Salir
+
+
+ )}
{/* Close dropdown when tapping outside */}
- setShowMenu(false)} style={styles.touchOutside}>
+ setShowMenu(false)}
+ style={styles.touchOutside}
+ activeOpacity={1}
+ >
String(p.id)}
@@ -147,43 +151,40 @@ export function ProjectsScreen({ navigation }: Props) {
}
const styles = StyleSheet.create({
- container: { flex: 1 },
- header: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- padding: 16,
+ container: { flex: 1, backgroundColor: '#fff' },
+ headerRight: {
+ marginRight: 8,
},
- headerTitle: { fontSize: 20, fontWeight: '700', color: COLORS.primary },
- dropdownWrapper: {
- position: 'relative',
+ userButton: {
+ paddingHorizontal: 8,
+ paddingVertical: 4,
},
dropdownLabel: {
- fontSize: 16,
+ fontSize: 15,
fontWeight: '600',
- color: '#333',
+ color: COLORS.primary,
},
- chevron: { fontSize: 14, color: '#666' },
- dropdownMenu: {
+ chevronSmall: { fontSize: 10, color: COLORS.primary },
+ floatingMenu: {
position: 'absolute',
- top: 40,
- right: 0,
+ top: 4,
+ right: 16,
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: COLORS.border,
overflow: 'hidden',
minWidth: 180,
- elevation: 4,
+ elevation: 10,
shadowColor: '#000',
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 4,
- zIndex: 100,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.2,
+ shadowRadius: 5,
+ zIndex: 1000,
},
dropdownItem: {
paddingHorizontal: 16,
- paddingVertical: 12,
+ paddingVertical: 14,
},
dropdownItemDanger: {
borderTopWidth: StyleSheet.hairlineWidth,
@@ -201,13 +202,15 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
- paddingVertical: 16,
- borderTopWidth: StyleSheet.hairlineWidth,
+ paddingVertical: 18,
+ borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLORS.border,
backgroundColor: '#fff',
},
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 },
+ chevron: { fontSize: 20, color: COLORS.border, marginLeft: 8 },
empty: { textAlign: 'center', marginTop: 40, color: '#888' },
});
+
diff --git a/src/screens/detail/FeatureDetailContent.tsx b/src/screens/detail/FeatureDetailContent.tsx
index b233b9c..7b3b4fe 100644
--- a/src/screens/detail/FeatureDetailContent.tsx
+++ b/src/screens/detail/FeatureDetailContent.tsx
@@ -26,6 +26,34 @@ export function FeatureDetailContent({ featureId }: { featureId: number }) {
const [inspections, setInspections] = useState([]);
const [featureTypes, setFeatureTypes] = useState([]);
+ // 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: () => (
+
+ navigation.navigate('InspectionForm', {
+ featureId: feature.id,
+ featureName: feature.name,
+ templateId: feature.template_id ?? undefined,
+ })
+ }
+ >
+ + Inspección
+
+ ),
+ });
+ }
+ return () => {
+ navigation.setOptions({ headerRight: undefined });
+ };
+ }, [navigation, canInspect, feature]),
+ );
+
const refresh = useCallback(async () => {
const [f, ins, types] = await Promise.all([
getFeature(featureId),
@@ -52,33 +80,6 @@ export function FeatureDetailContent({ featureId }: { featureId: number }) {
const featureType = featureTypes.find((t) => t.id === feature.feature_type_id);
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: () => (
-
- navigation.navigate('InspectionForm', {
- featureId: feature.id,
- featureName: feature.name,
- templateId: feature.template_id ?? undefined,
- })
- }
- >
- + Inspección
-
- ),
- });
- }
- return () => {
- navigation.setOptions({ headerRight: undefined });
- };
- }, [navigation, canInspect, feature?.id, feature?.name, feature?.template_id]),
- );
-
return (