feat(android): app de campo móvil+tablet (UI, mapa, fotos, auto-sync, reconciliación)

Fase 0: config Android (package, permisos, orientación), eas.json (APK), deps
  nativas (react-native-maps, image-picker, location), app.config.js para la key
  de Google Maps por secreto EAS.
Fase 1: capa responsive (useLayout) + componente MasterDetail (dos paneles en
  tablet, navegación en móvil).
Fase 2: pantallas funcionales — detalle de proyecto con secciones Fases/Features/
  Incidencias, edición de progreso/estado, formulario de inspección dinámico
  desde plantilla, incidencias maestro-detalle (checklist + comentarios), alta de
  incidencia; gating por permisos Spatie.
Fase 3: fotos (cámara/galería) → cola de media, con miniaturas pendientes/sync.
Fase 4: mapa de features (Google Maps) con geometría GeoJSON y selección.
Fase 5: auto-sync (foreground/reconexión/intervalo, con candado) + reconciliación
  de creaciones offline (id temporal negativo → server_id, remapeo de FKs hijas).
Fase 6: revisión de conflictos/errores del outbox, indicadores y APK preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
javier
2026-06-18 18:30:44 +02:00
co-authored by Claude Opus 4.8
parent 4e9c7d059f
commit 9bcc51e3b2
36 changed files with 2532 additions and 131 deletions
+17 -3
View File
@@ -1,19 +1,22 @@
import React from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { OutboxCounts } from '../db/outbox';
import { useIsOnline } from '../net/connectivity';
export function SyncStatusBar({
counts,
syncing,
onPress,
}: {
counts: OutboxCounts;
syncing: boolean;
onPress?: () => void;
}) {
const online = useIsOnline();
const pendientes = counts.pending + counts.mediaPending;
const hasProblems = counts.conflict > 0 || counts.error > 0;
return (
const content = (
<View style={[styles.bar, { backgroundColor: online ? '#1f6f43' : '#8a6d00' }]}>
{syncing && <ActivityIndicator color="#fff" size="small" />}
<Text style={styles.text}>
@@ -22,8 +25,18 @@ export function SyncStatusBar({
{counts.conflict > 0 ? ` · ${counts.conflict} conflicto(s)` : ''}
{counts.error > 0 ? ` · ${counts.error} error(es)` : ''}
</Text>
{hasProblems && onPress ? <Text style={styles.chevron}>revisar </Text> : null}
</View>
);
if (onPress) {
return (
<TouchableOpacity activeOpacity={0.8} onPress={onPress}>
{content}
</TouchableOpacity>
);
}
return content;
}
const styles = StyleSheet.create({
@@ -34,5 +47,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 12,
paddingVertical: 8,
},
text: { color: '#fff', fontSize: 13, fontWeight: '600' },
text: { color: '#fff', fontSize: 13, fontWeight: '600', flex: 1 },
chevron: { color: '#fff', fontSize: 12, fontWeight: '700', textDecorationLine: 'underline' },
});