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 (