KI-AGENT: Share-Upload an FEDEO-Design und Listenfilter angleichen

This commit is contained in:
2026-08-08 22:36:09 +02:00
parent ec4469e21f
commit ca91de2284
5 changed files with 166 additions and 130 deletions

View File

@@ -26,6 +26,13 @@ import {
uploadProjectFile,
} from '@/src/lib/api';
import { useAuth } from '@/src/providers/auth-provider';
import {
filterCustomers,
filterPlants,
filterProjects,
getActiveProjectPhase,
getPlantCustomerName,
} from '@/src/lib/resource-list-filters';
type TargetType = 'project' | 'customer' | 'plant';
type UploadTarget = Project | Customer | Plant;
@@ -36,6 +43,8 @@ const TARGET_TYPES: { key: TargetType; label: string; plural: string; icon: keyo
{ key: 'plant', label: 'Objekt', plural: 'Objekte', icon: 'business-outline' },
];
const PRIMARY = '#69c350';
export default function ShareUploadScreen() {
const router = useRouter();
const { token } = useAuth();
@@ -60,10 +69,10 @@ export default function ShareUploadScreen() {
setSearch('');
const request = targetType === 'project'
? fetchProjects(token)
? fetchProjects(token, true)
: targetType === 'customer'
? fetchCustomers(token)
: fetchPlants(token);
? fetchCustomers(token, true)
: fetchPlants(token, true);
void request
.then((rows) => {
@@ -82,17 +91,11 @@ export default function ShareUploadScreen() {
}, [targetType, token]);
const filteredTargets = useMemo(() => {
const term = search.trim().toLocaleLowerCase('de');
if (!term) return targets;
return targets.filter((target) => {
const number = 'projectNumber' in target
? target.projectNumber
: 'customerNumber' in target
? target.customerNumber
: null;
return `${target.name} ${number || ''}`.toLocaleLowerCase('de').includes(term);
});
}, [search, targets]);
if (targetType === 'project') return filterProjects(targets as Project[], search);
if (targetType === 'customer') return filterCustomers(targets as Customer[], search);
if (targetType === 'plant') return filterPlants(targets as Plant[], search);
return [];
}, [search, targets, targetType]);
const close = () => {
resetShareIntent();
@@ -144,7 +147,9 @@ export default function ShareUploadScreen() {
return (
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
<View style={styles.emptyState}>
<Ionicons name="document-outline" size={44} color="#9ca3af" />
<View style={styles.emptyIcon}>
<Ionicons name="document-outline" size={36} color={PRIMARY} />
</View>
<Text style={styles.emptyTitle}>Keine Dateien gefunden</Text>
<Text style={styles.emptyText}>Teile eine Datei oder ein Foto erneut mit FEDEO.</Text>
<Pressable style={styles.primaryButton} onPress={close}>
@@ -159,7 +164,9 @@ export default function ShareUploadScreen() {
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
<View style={styles.fileSummary}>
<Ionicons name="documents-outline" size={24} color="#2563eb" />
<View style={styles.summaryIcon}>
<Ionicons name="documents-outline" size={22} color="#3d7a30" />
</View>
<View style={styles.flex}>
<Text style={styles.summaryTitle}>{files.length} {files.length === 1 ? 'Datei' : 'Dateien'} ausgewählt</Text>
<Text style={styles.summaryText} numberOfLines={2}>{files.map((file) => file.fileName).join(', ')}</Text>
@@ -173,7 +180,7 @@ export default function ShareUploadScreen() {
<View style={styles.typeGrid}>
{TARGET_TYPES.map((type) => (
<Pressable key={type.key} style={styles.typeCard} onPress={() => setTargetType(type.key)}>
<View style={styles.iconCircle}><Ionicons name={type.icon} size={26} color="#2563eb" /></View>
<View style={styles.iconCircle}><Ionicons name={type.icon} size={24} color="#3d7a30" /></View>
<Text style={styles.typeLabel}>{type.label}</Text>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</Pressable>
@@ -183,21 +190,24 @@ export default function ShareUploadScreen() {
) : (
<>
<Pressable style={styles.backLink} onPress={() => setTargetType(null)} disabled={isUploading}>
<Ionicons name="chevron-back" size={18} color="#2563eb" />
<Ionicons name="chevron-back" size={18} color="#3d7a30" />
<Text style={styles.backLinkText}>Andere Zielart wählen</Text>
</Pressable>
<Text style={styles.heading}>{selectedType?.label} auswählen</Text>
<TextInput
value={search}
onChangeText={setSearch}
placeholder={`${selectedType?.plural || 'Ziele'} durchsuchen`}
placeholderTextColor="#9ca3af"
style={styles.searchInput}
editable={!isUploading}
/>
<View style={styles.searchBox}>
<Ionicons name="search" size={19} color="#6b7280" />
<TextInput
value={search}
onChangeText={setSearch}
placeholder={`${selectedType?.plural || 'Ziele'} durchsuchen`}
placeholderTextColor="#9ca3af"
style={styles.searchInput}
editable={!isUploading}
/>
</View>
{isLoading ? (
<ActivityIndicator style={styles.loader} color="#2563eb" />
<ActivityIndicator style={styles.loader} color={PRIMARY} />
) : loadError ? (
<Text style={styles.errorText}>{loadError}</Text>
) : filteredTargets.length === 0 ? (
@@ -210,13 +220,29 @@ export default function ShareUploadScreen() {
: 'customerNumber' in target
? target.customerNumber
: null;
const detail = targetType === 'project'
? getActiveProjectPhase(target as Project)
: targetType === 'plant'
? getPlantCustomerName((target as Plant).customer)
: null;
return (
<Pressable key={target.id} style={styles.targetRow} onPress={() => void uploadTo(target)} disabled={isUploading}>
<Pressable
key={target.id}
style={({ pressed }) => [styles.targetRow, pressed ? styles.targetRowPressed : null]}
onPress={() => void uploadTo(target)}
disabled={isUploading}>
<View style={styles.flex}>
<Text style={styles.targetName}>{target.name}</Text>
{number ? <Text style={styles.targetNumber}>{String(number)}</Text> : null}
{number ? <Text style={styles.targetNumber}>Nr.: {String(number)}</Text> : null}
{detail ? (
<Text style={styles.targetNumber} numberOfLines={1}>
{targetType === 'project' ? `Phase: ${detail}` : `Kunde: ${detail}`}
</Text>
) : null}
</View>
<View style={styles.uploadIcon}>
<Ionicons name="cloud-upload-outline" size={21} color="#3d7a30" />
</View>
<Ionicons name="cloud-upload-outline" size={22} color="#2563eb" />
</Pressable>
);
})}
@@ -227,7 +253,7 @@ export default function ShareUploadScreen() {
{isUploading ? (
<View style={styles.uploadOverlay}>
<ActivityIndicator color="#2563eb" />
<ActivityIndicator color={PRIMARY} />
<Text style={styles.uploadText}>Upload läuft: {uploadedCount} von {files.length}</Text>
</View>
) : null}
@@ -241,35 +267,40 @@ export default function ShareUploadScreen() {
}
const styles = StyleSheet.create({
safeArea: { flex: 1, backgroundColor: '#f9fafb' },
content: { padding: 20, paddingBottom: 36, gap: 16 },
safeArea: { flex: 1, backgroundColor: '#ffffff' },
content: { paddingBottom: 28, gap: 14 },
flex: { flex: 1 },
fileSummary: { flexDirection: 'row', gap: 12, alignItems: 'center', padding: 16, borderRadius: 14, backgroundColor: '#eff6ff' },
summaryTitle: { fontSize: 16, fontWeight: '700', color: '#1e3a8a' },
summaryText: { marginTop: 3, fontSize: 13, color: '#3b4f78' },
heading: { marginTop: 8, fontSize: 23, fontWeight: '800', color: '#111827' },
description: { marginTop: -10, fontSize: 15, color: '#6b7280' },
typeGrid: { gap: 12 },
typeCard: { flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16, borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 14, backgroundColor: '#fff' },
iconCircle: { width: 46, height: 46, borderRadius: 23, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff' },
typeLabel: { flex: 1, fontSize: 17, fontWeight: '700', color: '#111827' },
backLink: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', marginTop: 2 },
backLinkText: { fontSize: 14, fontWeight: '600', color: '#2563eb' },
searchInput: { height: 48, paddingHorizontal: 14, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 12, backgroundColor: '#fff', fontSize: 16, color: '#111827' },
fileSummary: { flexDirection: 'row', gap: 12, alignItems: 'center', paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#d8edcf', backgroundColor: '#eff9ea' },
summaryIcon: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff' },
summaryTitle: { fontSize: 15, fontWeight: '700', color: '#2f5f24' },
summaryText: { marginTop: 2, fontSize: 12, color: '#4f6f47' },
heading: { marginTop: 4, paddingHorizontal: 16, fontSize: 20, fontWeight: '700', color: '#111827' },
description: { marginTop: -8, paddingHorizontal: 16, fontSize: 14, color: '#6b7280' },
typeGrid: { borderTopWidth: 1, borderTopColor: '#e5e7eb' },
typeCard: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff' },
iconCircle: { width: 42, height: 42, borderRadius: 21, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
typeLabel: { flex: 1, fontSize: 15, fontWeight: '600', color: '#111827' },
backLink: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', marginHorizontal: 12, marginTop: 2, padding: 4 },
backLinkText: { fontSize: 14, fontWeight: '600', color: '#3d7a30' },
searchBox: { minHeight: 44, marginHorizontal: 16, flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 12, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, backgroundColor: '#ffffff' },
searchInput: { flex: 1, paddingVertical: 10, fontSize: 15, color: '#111827' },
loader: { marginVertical: 32 },
errorText: { padding: 16, borderRadius: 12, backgroundColor: '#fef2f2', color: '#b91c1c' },
errorText: { marginHorizontal: 16, padding: 12, borderRadius: 10, backgroundColor: '#fef2f2', color: '#b91c1c' },
emptyListText: { paddingVertical: 28, textAlign: 'center', color: '#6b7280' },
targetList: { overflow: 'hidden', borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 14, backgroundColor: '#fff' },
targetRow: { minHeight: 64, flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#d1d5db' },
targetName: { fontSize: 16, fontWeight: '600', color: '#111827' },
targetList: { borderTopWidth: 1, borderTopColor: '#e5e7eb', backgroundColor: '#ffffff' },
targetRow: { minHeight: 64, flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff' },
targetRowPressed: { backgroundColor: '#f3f4f6' },
targetName: { fontSize: 15, fontWeight: '600', color: '#111827' },
targetNumber: { marginTop: 3, fontSize: 13, color: '#6b7280' },
uploadOverlay: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, padding: 14, borderRadius: 12, backgroundColor: '#eff6ff' },
uploadText: { fontWeight: '600', color: '#1e3a8a' },
uploadIcon: { width: 38, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
uploadOverlay: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, marginHorizontal: 16, padding: 14, borderRadius: 10, backgroundColor: '#eff9ea' },
uploadText: { fontWeight: '600', color: '#2f5f24' },
cancelButton: { alignItems: 'center', padding: 14 },
cancelButtonText: { fontSize: 16, fontWeight: '600', color: '#b91c1c' },
emptyState: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 28 },
emptyIcon: { width: 64, height: 64, borderRadius: 32, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
emptyTitle: { marginTop: 14, fontSize: 20, fontWeight: '700', color: '#111827' },
emptyText: { marginTop: 6, textAlign: 'center', color: '#6b7280' },
primaryButton: { marginTop: 22, paddingHorizontal: 18, paddingVertical: 13, borderRadius: 11, backgroundColor: '#2563eb' },
primaryButton: { marginTop: 22, paddingHorizontal: 18, paddingVertical: 13, borderRadius: 10, backgroundColor: PRIMARY },
primaryButtonText: { fontSize: 15, fontWeight: '700', color: '#fff' },
});