import { NativeStackScreenProps } from '@react-navigation/native-stack'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, FlatList, RefreshControl, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import * as api from '../api/endpoints'; import { Project } from '../api/types'; import { useSession } from '../auth/session'; import { getProjects, saveProjectList, saveTemplates, setActiveProjectId } from '../db/repositories'; import { isOnline } from '../net/connectivity'; import { runSync } from '../sync/engine'; import { RootStackParamList } from '../navigation/types'; import { COLORS, PrimaryButton } from '../ui/components'; type Props = NativeStackScreenProps; /** Tolerante con la forma de la respuesta: `{projects}` (actual), `{data}` o array. */ function normalize(res: unknown): Project[] { if (Array.isArray(res)) return res as Project[]; const obj = (res ?? {}) as { projects?: Project[]; data?: Project[] }; return obj.projects ?? obj.data ?? []; } export function ProjectsScreen({ navigation }: Props) { const { user, signOut } = useSession(); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); const [opening, setOpening] = useState(null); const [showMenu, setShowMenu] = useState(false); const dropdownRef = useRef(null); const load = useCallback(async () => { setLoading(true); try { 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); } catch (e) { console.warn('No se pudieron prefetchar las plantillas:', e); } } setProjects(await getProjects()); } catch (e) { console.warn('No se pudo refrescar /projects, usando datos locales:', e); setProjects(await getProjects()); // fallback offline } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const openProject = useCallback( async (p: Project) => { 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 { setOpening(null); } }, [navigation], ); const handleLogout = () => { setShowMenu(false); signOut(); }; const handleSettings = () => { setShowMenu(false); navigation.navigate('Settings'); }; return ( {/* Header with dropdown menu */} Proyectos setShowMenu(!showMenu)} activeOpacity={1} > {user?.name ?? 'Usuario'} {showMenu && ( ⚙ Configuración Salir )} {/* Close dropdown when tapping outside */} setShowMenu(false)} style={styles.touchOutside}> String(p.id)} refreshControl={} ListEmptyComponent={loading ? null : No hay proyectos.} renderItem={({ item }) => ( openProject(item)} activeOpacity={0.7}> {item.name} {item.reference ? Ref: {item.reference} : null} {item.address ? 📍 {item.address} : null} {item.status ? Estado: {item.status} : null} {opening === item.id ? ( ) : ( )} )} /> ); } const styles = StyleSheet.create({ container: { flex: 1 }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16, }, headerTitle: { fontSize: 20, fontWeight: '700', color: COLORS.primary }, dropdownWrapper: { position: 'relative', }, dropdownLabel: { fontSize: 16, fontWeight: '600', color: '#333', }, chevron: { fontSize: 14, color: '#666' }, dropdownMenu: { position: 'absolute', top: 40, right: 0, backgroundColor: '#fff', borderRadius: 8, borderWidth: 1, borderColor: COLORS.border, overflow: 'hidden', minWidth: 180, elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, zIndex: 100, }, dropdownItem: { paddingHorizontal: 16, paddingVertical: 12, }, dropdownItemDanger: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: COLORS.border, }, dropdownItemText: { fontSize: 15, color: '#333', }, touchOutside: { flex: 1, }, card: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 16, borderTopWidth: StyleSheet.hairlineWidth, borderColor: COLORS.border, backgroundColor: '#fff', }, cardContent: { flex: 1 }, cardName: { fontSize: 16, fontWeight: '600', color: '#111' }, cardMeta: { fontSize: 13, color: COLORS.muted, marginTop: 4 }, empty: { textAlign: 'center', marginTop: 40, color: '#888' }, });