307 lines
14 KiB
TypeScript
307 lines
14 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons';
|
|
import { useRouter } from 'expo-router';
|
|
import { useShareIntentContext } from 'expo-share-intent';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
Alert,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
|
import {
|
|
Customer,
|
|
fetchCustomers,
|
|
fetchPlants,
|
|
fetchProjects,
|
|
Plant,
|
|
Project,
|
|
uploadCustomerFile,
|
|
uploadPlantFile,
|
|
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;
|
|
|
|
const TARGET_TYPES: { key: TargetType; label: string; plural: string; icon: keyof typeof Ionicons.glyphMap }[] = [
|
|
{ key: 'project', label: 'Projekt', plural: 'Projekte', icon: 'folder-outline' },
|
|
{ key: 'customer', label: 'Kunde', plural: 'Kunden', icon: 'people-outline' },
|
|
{ key: 'plant', label: 'Objekt', plural: 'Objekte', icon: 'business-outline' },
|
|
];
|
|
|
|
const PRIMARY = '#69c350';
|
|
|
|
export default function ShareUploadScreen() {
|
|
const router = useRouter();
|
|
const { token } = useAuth();
|
|
const { shareIntent, resetShareIntent } = useShareIntentContext();
|
|
const [targetType, setTargetType] = useState<TargetType | null>(null);
|
|
const [targets, setTargets] = useState<UploadTarget[]>([]);
|
|
const [search, setSearch] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const [uploadedCount, setUploadedCount] = useState(0);
|
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
|
|
const files = shareIntent.files || [];
|
|
const selectedType = TARGET_TYPES.find((type) => type.key === targetType);
|
|
|
|
useEffect(() => {
|
|
if (!targetType || !token) return;
|
|
|
|
let isActive = true;
|
|
setIsLoading(true);
|
|
setLoadError(null);
|
|
setSearch('');
|
|
|
|
const request = targetType === 'project'
|
|
? fetchProjects(token, true)
|
|
: targetType === 'customer'
|
|
? fetchCustomers(token, true)
|
|
: fetchPlants(token, true);
|
|
|
|
void request
|
|
.then((rows) => {
|
|
if (isActive) setTargets(rows.sort((a, b) => a.name.localeCompare(b.name, 'de')));
|
|
})
|
|
.catch((error) => {
|
|
if (isActive) setLoadError(error instanceof Error ? error.message : 'Die Ziele konnten nicht geladen werden.');
|
|
})
|
|
.finally(() => {
|
|
if (isActive) setIsLoading(false);
|
|
});
|
|
|
|
return () => {
|
|
isActive = false;
|
|
};
|
|
}, [targetType, token]);
|
|
|
|
const filteredTargets = useMemo(() => {
|
|
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();
|
|
router.replace('/');
|
|
};
|
|
|
|
const confirmCancel = () => {
|
|
Alert.alert('Upload abbrechen?', 'Die geteilten Dateien werden nicht hochgeladen.', [
|
|
{ text: 'Weiter auswählen', style: 'cancel' },
|
|
{ text: 'Abbrechen', style: 'destructive', onPress: close },
|
|
]);
|
|
};
|
|
|
|
const uploadTo = async (target: UploadTarget) => {
|
|
if (!token || !targetType || files.length === 0 || isUploading) return;
|
|
|
|
setIsUploading(true);
|
|
setUploadedCount(0);
|
|
try {
|
|
for (const [index, file] of files.entries()) {
|
|
const common = {
|
|
uri: file.path,
|
|
filename: file.fileName || `Geteilte Datei ${index + 1}`,
|
|
mimeType: file.mimeType || undefined,
|
|
};
|
|
|
|
if (targetType === 'project') {
|
|
await uploadProjectFile(token, { ...common, projectId: target.id });
|
|
} else if (targetType === 'customer') {
|
|
await uploadCustomerFile(token, { ...common, customerId: target.id });
|
|
} else {
|
|
await uploadPlantFile(token, { ...common, plantId: target.id });
|
|
}
|
|
setUploadedCount(index + 1);
|
|
}
|
|
|
|
resetShareIntent();
|
|
Alert.alert('Upload abgeschlossen', `${files.length} ${files.length === 1 ? 'Datei wurde' : 'Dateien wurden'} zu „${target.name}“ hochgeladen.`, [
|
|
{ text: 'Fertig', onPress: () => router.replace('/') },
|
|
]);
|
|
} catch (error) {
|
|
Alert.alert('Upload fehlgeschlagen', error instanceof Error ? error.message : 'Die Datei konnte nicht hochgeladen werden.');
|
|
} finally {
|
|
setIsUploading(false);
|
|
}
|
|
};
|
|
|
|
if (files.length === 0) {
|
|
return (
|
|
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
|
|
<View style={styles.emptyState}>
|
|
<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}>
|
|
<Text style={styles.primaryButtonText}>Zurück zu FEDEO</Text>
|
|
</Pressable>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
|
|
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
|
<View style={styles.fileSummary}>
|
|
<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>
|
|
</View>
|
|
</View>
|
|
|
|
{!targetType ? (
|
|
<>
|
|
<Text style={styles.heading}>Wohin möchtest du hochladen?</Text>
|
|
<Text style={styles.description}>Wähle zuerst die Art des Ziels aus.</Text>
|
|
<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={24} color="#3d7a30" /></View>
|
|
<Text style={styles.typeLabel}>{type.label}</Text>
|
|
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Pressable style={styles.backLink} onPress={() => setTargetType(null)} disabled={isUploading}>
|
|
<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>
|
|
<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={PRIMARY} />
|
|
) : loadError ? (
|
|
<Text style={styles.errorText}>{loadError}</Text>
|
|
) : filteredTargets.length === 0 ? (
|
|
<Text style={styles.emptyListText}>Keine passenden {selectedType?.plural.toLocaleLowerCase('de')} gefunden.</Text>
|
|
) : (
|
|
<View style={styles.targetList}>
|
|
{filteredTargets.map((target) => {
|
|
const number = 'projectNumber' in target
|
|
? target.projectNumber
|
|
: '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={({ 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}>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>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{isUploading ? (
|
|
<View style={styles.uploadOverlay}>
|
|
<ActivityIndicator color={PRIMARY} />
|
|
<Text style={styles.uploadText}>Upload läuft: {uploadedCount} von {files.length}</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
<Pressable style={styles.cancelButton} onPress={confirmCancel} disabled={isUploading}>
|
|
<Text style={styles.cancelButtonText}>Abbrechen</Text>
|
|
</Pressable>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
safeArea: { flex: 1, backgroundColor: '#ffffff' },
|
|
content: { paddingBottom: 28, gap: 14 },
|
|
flex: { flex: 1 },
|
|
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: { marginHorizontal: 16, padding: 12, borderRadius: 10, backgroundColor: '#fef2f2', color: '#b91c1c' },
|
|
emptyListText: { paddingVertical: 28, textAlign: 'center', color: '#6b7280' },
|
|
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' },
|
|
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: 10, backgroundColor: PRIMARY },
|
|
primaryButtonText: { fontSize: 15, fontWeight: '700', color: '#fff' },
|
|
});
|