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:
co-authored by
Claude Opus 4.8
parent
4e9c7d059f
commit
9bcc51e3b2
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Revisión del outbox: operaciones en conflicto o con error. Permite reintentar
|
||||
* (volver a encolar) o descartar. Los conflictos muestran el valor del servidor.
|
||||
*/
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { discardOp, getProblemOps, ProblemOp, retryOp } from '../db/outbox';
|
||||
import { Badge, Card, COLORS, EmptyState, PrimaryButton } from '../ui/components';
|
||||
|
||||
export function OutboxScreen() {
|
||||
const [ops, setOps] = useState<ProblemOp[]>([]);
|
||||
|
||||
const load = useCallback(() => {
|
||||
void getProblemOps().then(setOps);
|
||||
}, []);
|
||||
|
||||
useFocusEffect(load);
|
||||
|
||||
const onRetry = useCallback(
|
||||
async (uuid: string) => {
|
||||
await retryOp(uuid);
|
||||
load();
|
||||
},
|
||||
[load],
|
||||
);
|
||||
const onDiscard = useCallback(
|
||||
async (uuid: string) => {
|
||||
await discardOp(uuid);
|
||||
load();
|
||||
},
|
||||
[load],
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.body}>
|
||||
{ops.length === 0 && <EmptyState text="Nada pendiente de revisión." />}
|
||||
{ops.map((o) => (
|
||||
<Card key={o.uuid} style={styles.card}>
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.entity}>
|
||||
{o.entity}.{o.op}
|
||||
</Text>
|
||||
<Badge
|
||||
label={o.status}
|
||||
color={o.status === 'conflict' ? COLORS.warn : COLORS.danger}
|
||||
/>
|
||||
</View>
|
||||
{o.error ? <Text style={styles.error}>{o.error}</Text> : null}
|
||||
{o.server_payload ? (
|
||||
<Text style={styles.mono} numberOfLines={6}>
|
||||
Servidor: {o.server_payload}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={styles.mono} numberOfLines={6}>
|
||||
Local: {o.data}
|
||||
</Text>
|
||||
<View style={styles.actions}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<PrimaryButton title="Reintentar" variant="ghost" onPress={() => void onRetry(o.uuid)} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<PrimaryButton title="Descartar" variant="danger" onPress={() => void onDiscard(o.uuid)} />
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
body: { padding: 16, gap: 12 },
|
||||
card: { gap: 8 },
|
||||
headerRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
entity: { fontSize: 15, fontWeight: '700' },
|
||||
error: { color: COLORS.danger, fontSize: 13 },
|
||||
mono: { fontSize: 12, color: COLORS.muted, fontFamily: 'monospace' },
|
||||
actions: { flexDirection: 'row', gap: 8, marginTop: 4 },
|
||||
});
|
||||
Reference in New Issue
Block a user