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:
@@ -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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user