feat(ui): quitar pestaña Fases + selector de plantilla al inspeccionar

- ProjectDetail: pestañas reducidas a Features | Incidencias (Features por
  defecto); PhasesSection eliminada.
- InspectionForm en dos pasos: 1) selector de plantillas asignadas al
  proyecto (la de la feature marcada como sugerida ★, opción de inspección
  libre), 2) formulario dinámico. Enlace "Cambiar plantilla" para volver.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 10:50:20 +02:00
co-authored by Claude Sonnet 4.6
parent 9c4c3b54ae
commit 190b4f6f34
3 changed files with 100 additions and 104 deletions
+98 -7
View File
@@ -8,9 +8,9 @@
*/
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
import { Alert, ScrollView, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { Template } from '../api/types';
import { getTemplate } from '../db/repositories';
import { getTemplates } from '../db/repositories';
import { createInspection } from '../sync/mutations';
import { RootStackParamList } from '../navigation/types';
import {
@@ -75,7 +75,12 @@ function groupFields(fields: NormField[]): { group: string; items: NormField[] }
const RESULTS = ['pass', 'fail', 'na'] as const;
export function InspectionFormScreen({ route, navigation }: Props) {
const { featureId, featureName, templateId } = route.params;
const { featureId, featureName, templateId: suggestedId } = route.params;
// Paso 1: elegir plantilla (las asignadas al proyecto llegan en el bundle).
// Paso 2: rellenar el formulario. `chosen` = false → aún en el selector.
const [available, setAvailable] = useState<Template[]>([]);
const [chosen, setChosen] = useState(false);
const [template, setTemplate] = useState<Template | null>(null);
const [values, setValues] = useState<Record<string, unknown>>({});
const [result, setResult] = useState<(typeof RESULTS)[number] | undefined>();
@@ -83,8 +88,27 @@ export function InspectionFormScreen({ route, navigation }: Props) {
const [saving, setSaving] = useState(false);
useEffect(() => {
if (templateId != null) void getTemplate(templateId).then(setTemplate);
}, [templateId]);
void getTemplates().then((all) => {
// La plantilla sugerida (la de la feature) primero.
all.sort((a, b) =>
Number(b.id === suggestedId) - Number(a.id === suggestedId) ||
a.name.localeCompare(b.name),
);
setAvailable(all);
});
}, [suggestedId]);
const pickTemplate = useCallback((t: Template | null) => {
setTemplate(t);
setValues({});
setChosen(true);
}, []);
const backToPicker = useCallback(() => {
setChosen(false);
setTemplate(null);
setValues({});
}, []);
const fields: NormField[] = (template?.fields ?? []).map(normalizeField);
@@ -111,7 +135,7 @@ export function InspectionFormScreen({ route, navigation }: Props) {
try {
await createInspection({
feature_id: featureId,
template_id: templateId,
template_id: template?.id ?? undefined,
data: values,
result,
notes: notes.trim() || undefined,
@@ -122,7 +146,7 @@ export function InspectionFormScreen({ route, navigation }: Props) {
setSaving(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [featureId, templateId, values, result, notes, navigation, template]);
}, [featureId, values, result, notes, navigation, template]);
const renderField = (f: NormField) => {
const label = f.required ? `${f.label} *` : f.label;
@@ -162,10 +186,61 @@ export function InspectionFormScreen({ route, navigation }: Props) {
);
};
// ── Paso 1: selector de plantilla ──
if (!chosen) {
return (
<ScrollView contentContainerStyle={styles.body}>
<Text style={styles.subtitle}>{featureName}</Text>
<Text style={styles.tplName}>Elige una plantilla</Text>
{available.length === 0 && (
<Text style={styles.noTemplates}>
No hay plantillas asignadas a este proyecto. Sincroniza o crea una
inspección libre.
</Text>
)}
{available.map((t) => (
<TouchableOpacity key={t.id} style={styles.tplCard} onPress={() => pickTemplate(t)}>
<View style={{ flex: 1 }}>
<Text style={styles.tplCardName}>
{t.name}
{t.id === suggestedId ? ' ★' : ''}
</Text>
{t.description ? (
<Text style={styles.tplCardDesc} numberOfLines={2}>
{t.description}
</Text>
) : null}
<Text style={styles.tplCardMeta}>
{(t.fields ?? []).length} campos
{t.id === suggestedId ? ' · sugerida para esta feature' : ''}
</Text>
</View>
<Text style={styles.tplChevron}></Text>
</TouchableOpacity>
))}
<View style={{ height: 8 }} />
<PrimaryButton
title="Inspección libre (sin plantilla)"
variant="ghost"
onPress={() => pickTemplate(null)}
/>
<View style={{ height: 8 }} />
<PrimaryButton title="Cancelar" variant="ghost" onPress={() => navigation.goBack()} />
</ScrollView>
);
}
// ── Paso 2: formulario ──
return (
<ScrollView contentContainerStyle={styles.body}>
<Text style={styles.subtitle}>{featureName}</Text>
<Text style={styles.tplName}>{template?.name ?? 'Inspección libre'}</Text>
<TouchableOpacity onPress={backToPicker}>
<Text style={styles.changeTpl}> Cambiar plantilla</Text>
</TouchableOpacity>
{groupFields(fields).map((section, si) => (
<View key={`g${si}`}>
@@ -205,4 +280,20 @@ const styles = StyleSheet.create({
},
switchLabel: { fontSize: 15, flex: 1 },
help: { fontSize: 12, color: COLORS.muted, marginTop: -6, marginBottom: 8 },
changeTpl: { color: COLORS.primary, fontSize: 13, fontWeight: '600', marginBottom: 12 },
noTemplates: { color: COLORS.muted, fontSize: 14, marginVertical: 16, textAlign: 'center' },
tplCard: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: COLORS.border,
borderRadius: 10,
padding: 14,
marginBottom: 8,
backgroundColor: '#fff',
},
tplCardName: { fontSize: 15, fontWeight: '700' },
tplCardDesc: { fontSize: 13, color: COLORS.muted, marginTop: 2 },
tplCardMeta: { fontSize: 11, color: COLORS.muted, marginTop: 4 },
tplChevron: { fontSize: 24, color: COLORS.muted, marginLeft: 8 },
});