Fixes
This commit is contained in:
396
mobile/app/more/plant/[id].tsx
Normal file
396
mobile/app/more/plant/[id].tsx
Normal file
@@ -0,0 +1,396 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user