397 lines
11 KiB
TypeScript
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,
|
|
},
|
|
});
|