Files
FEDEO/mobile/app/more/plant/[id].tsx
2026-03-16 20:46:26 +01:00

397 lines
11 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
import { router, useLocalSearchParams } from 'expo-router';
import * as DocumentPicker from 'expo-document-picker';
import * as ImagePicker from 'expo-image-picker';
import * as WebBrowser from 'expo-web-browser';
import { fetchPlantById, fetchPlantFiles, Plant, ProjectFile, uploadPlantFile } from '@/src/lib/api';
import { useAuth } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
function formatDateTime(value: unknown): string {
if (!value) return '-';
const date = new Date(String(value));
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function getCustomerName(raw: Plant['customer']): string {
if (!raw) return '-';
if (typeof raw === 'object') return String(raw.name || raw.id || '-');
return String(raw);
}
export default function PlantDetailScreen() {
const params = useLocalSearchParams<{ id?: string }>();
const plantId = Number(params.id);
const { token } = useAuth();
const [plant, setPlant] = useState<Plant | null>(null);
const [files, setFiles] = useState<ProjectFile[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const validId = useMemo(() => Number.isFinite(plantId) && plantId > 0, [plantId]);
const rows = useMemo(() => {
if (!plant) return [];
return [
{ label: 'Name', value: String(plant.name || '-') },
{ label: 'Kunde', value: getCustomerName(plant.customer) },
{ label: 'Typ', value: String(plant.type || '-') },
{ label: 'Stadt', value: String(plant.city || '-') },
{ label: 'Strasse', value: String(plant.street || '-') },
{ label: 'PLZ', value: String(plant.zip || '-') },
{ label: 'Erstellt', value: formatDateTime(plant.createdAt || plant.created_at) },
{ label: 'Aktualisiert', value: formatDateTime(plant.updatedAt || plant.updated_at) },
];
}, [plant]);
const load = useCallback(async (showSpinner = true) => {
if (!token || !validId) return;
if (showSpinner) setLoading(true);
setError(null);
try {
const [plantData, fileData] = await Promise.all([
fetchPlantById(token, plantId),
fetchPlantFiles(token, plantId),
]);
setPlant(plantData);
setFiles(fileData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Objektdaten konnten nicht geladen werden.');
} finally {
setLoading(false);
setRefreshing(false);
}
}, [plantId, token, validId]);
useEffect(() => {
if (!token || !validId) return;
void load(true);
}, [load, token, validId]);
async function onRefresh() {
setRefreshing(true);
await load(false);
}
async function onOpenFile(file: ProjectFile) {
if (!file.url) return;
await WebBrowser.openBrowserAsync(file.url, {
presentationStyle: WebBrowser.WebBrowserPresentationStyle.FORM_SHEET,
controlsColor: PRIMARY,
showTitle: true,
enableDefaultShareMenuItem: true,
});
}
async function onPickAndUpload() {
if (!token || !validId || uploading) return;
const result = await DocumentPicker.getDocumentAsync({
multiple: false,
copyToCacheDirectory: true,
type: ['image/*', 'application/pdf', '*/*'],
});
if (result.canceled || !result.assets?.length) return;
const asset = result.assets[0];
const filename = asset.name || `upload-${Date.now()}`;
setUploading(true);
setError(null);
try {
await uploadPlantFile(token, {
plantId,
uri: asset.uri,
filename,
mimeType: asset.mimeType || 'application/octet-stream',
});
await load(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen.');
} finally {
setUploading(false);
}
}
async function uploadImageFromUri(uri: string, filename: string, mimeType?: string) {
if (!token || !validId) return;
setUploading(true);
setError(null);
try {
await uploadPlantFile(token, {
plantId,
uri,
filename,
mimeType: mimeType || 'image/jpeg',
});
await load(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen.');
} finally {
setUploading(false);
}
}
async function onPickImage() {
if (!token || !validId || uploading) return;
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) {
setError('Bitte erlaube den Zugriff auf deine Fotos.');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
quality: 0.85,
allowsEditing: false,
});
if (result.canceled || !result.assets?.length) return;
const asset = result.assets[0];
const filename = asset.fileName || `bild-${Date.now()}.jpg`;
await uploadImageFromUri(asset.uri, filename, asset.mimeType || 'image/jpeg');
}
async function onTakePhoto() {
if (!token || !validId || uploading) return;
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (!permission.granted) {
setError('Bitte erlaube den Zugriff auf die Kamera.');
return;
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ['images'],
quality: 0.85,
allowsEditing: false,
});
if (result.canceled || !result.assets?.length) return;
const asset = result.assets[0];
const filename = asset.fileName || `foto-${Date.now()}.jpg`;
await uploadImageFromUri(asset.uri, filename, asset.mimeType || 'image/jpeg');
}
return (
<ScrollView
contentContainerStyle={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
{error ? <Text style={styles.error}>{error}</Text> : null}
{loading ? (
<View style={styles.loadingBox}>
<ActivityIndicator />
<Text style={styles.loadingText}>Objekt wird geladen...</Text>
</View>
) : null}
{!loading && plant ? (
<>
<View style={styles.card}>
<View style={styles.sectionHeader}>
<Text style={styles.title}>Informationen</Text>
<Pressable
style={styles.uploadButton}
onPress={() =>
router.push({
pathname: '/more/wiki',
params: {
entityType: 'plants',
entityId: String(plantId),
title: `Objekt-Wiki: ${String(plant.name || plantId)}`,
},
})
}>
<Text style={styles.uploadButtonText}>Wiki</Text>
</Pressable>
</View>
<View style={styles.table}>
{rows.map((row) => (
<View key={row.label} style={styles.row}>
<Text style={styles.label}>{row.label}</Text>
<Text style={styles.value}>{row.value}</Text>
</View>
))}
</View>
</View>
<View style={styles.card}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Dokumente ({files.length})</Text>
</View>
<View style={styles.uploadActions}>
<Pressable
style={[styles.uploadButton, uploading ? styles.buttonDisabled : null]}
onPress={onTakePhoto}
disabled={uploading}>
<Text style={styles.uploadButtonText}>{uploading ? 'Upload...' : 'Foto aufnehmen'}</Text>
</Pressable>
<Pressable
style={[styles.uploadButton, uploading ? styles.buttonDisabled : null]}
onPress={onPickImage}
disabled={uploading}>
<Text style={styles.uploadButtonText}>{uploading ? 'Upload...' : 'Bild auswählen'}</Text>
</Pressable>
<Pressable
style={[styles.uploadButton, uploading ? styles.buttonDisabled : null]}
onPress={onPickAndUpload}
disabled={uploading}>
<Text style={styles.uploadButtonText}>{uploading ? 'Upload...' : 'Dokument hochladen'}</Text>
</Pressable>
</View>
{files.length === 0 ? (
<Text style={styles.empty}>Noch keine Dokumente vorhanden.</Text>
) : (
files.map((file) => (
<Pressable key={file.id} style={styles.fileRow} onPress={() => onOpenFile(file)}>
<View style={styles.fileInfo}>
<Text style={styles.fileName} numberOfLines={2}>
{file.name || file.path?.split('/').pop() || file.id}
</Text>
<Text style={styles.fileMeta}>{file.mimeType || 'Datei'}</Text>
</View>
</Pressable>
))
)}
</View>
</>
) : null}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
gap: 12,
backgroundColor: '#f9fafb',
},
card: {
backgroundColor: '#ffffff',
borderRadius: 12,
borderWidth: 1,
borderColor: '#e5e7eb',
padding: 12,
gap: 8,
},
title: {
color: '#111827',
fontSize: 18,
fontWeight: '700',
},
table: {
borderWidth: 1,
borderColor: '#e5e7eb',
borderRadius: 10,
overflow: 'hidden',
},
row: {
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
paddingHorizontal: 10,
paddingVertical: 8,
gap: 2,
},
label: {
color: '#6b7280',
fontSize: 12,
textTransform: 'uppercase',
fontWeight: '600',
},
value: {
color: '#111827',
fontSize: 14,
fontWeight: '500',
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 10,
},
sectionTitle: {
color: '#111827',
fontSize: 16,
fontWeight: '700',
},
uploadButton: {
minHeight: 38,
borderRadius: 9,
backgroundColor: PRIMARY,
paddingHorizontal: 12,
alignItems: 'center',
justifyContent: 'center',
},
uploadActions: {
gap: 8,
},
uploadButtonText: {
color: '#ffffff',
fontWeight: '700',
fontSize: 13,
},
fileRow: {
borderWidth: 1,
borderColor: '#e5e7eb',
borderRadius: 10,
padding: 10,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 10,
},
fileInfo: {
flex: 1,
minWidth: 0,
},
fileName: {
color: '#111827',
fontSize: 14,
fontWeight: '600',
},
fileMeta: {
color: '#6b7280',
fontSize: 12,
marginTop: 2,
},
empty: {
color: '#6b7280',
fontSize: 13,
paddingVertical: 4,
},
loadingBox: {
paddingVertical: 20,
alignItems: 'center',
gap: 8,
},
loadingText: {
color: '#6b7280',
},
error: {
color: '#dc2626',
fontSize: 13,
},
buttonDisabled: {
opacity: 0.6,
},
});