803 lines
23 KiB
TypeScript
803 lines
23 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
KeyboardAvoidingView,
|
|
Modal,
|
|
Platform,
|
|
Pressable,
|
|
RefreshControl,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
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 {
|
|
createProjectTask,
|
|
fetchProjectById,
|
|
fetchProjectFiles,
|
|
fetchProjectTasks,
|
|
Project,
|
|
ProjectFile,
|
|
Task,
|
|
TaskStatus,
|
|
uploadProjectFile,
|
|
updateTask,
|
|
} from '@/src/lib/api';
|
|
import { useAuth } from '@/src/providers/auth-provider';
|
|
|
|
const PRIMARY = '#69c350';
|
|
const TASK_STATUS_ORDER: TaskStatus[] = ['Offen', 'In Bearbeitung', 'Abgeschlossen'];
|
|
|
|
function normalizeTaskStatus(status: unknown): TaskStatus {
|
|
if (status === 'In Bearbeitung' || status === 'Abgeschlossen') return status;
|
|
return 'Offen';
|
|
}
|
|
|
|
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 getRefName(value: unknown): string {
|
|
if (!value) return '-';
|
|
if (typeof value === 'object') {
|
|
const record = value as { name?: string; full_name?: string; email?: string; id?: string | number };
|
|
return String(record.name || record.full_name || record.email || record.id || '-');
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
export default function ProjectDetailScreen() {
|
|
const params = useLocalSearchParams<{ id?: string }>();
|
|
const projectId = Number(params.id);
|
|
const { token, user } = useAuth();
|
|
|
|
const [project, setProject] = useState<Project | null>(null);
|
|
const [files, setFiles] = useState<ProjectFile[]>([]);
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [uploading, setUploading] = useState(false);
|
|
const [creatingTask, setCreatingTask] = useState(false);
|
|
const [updatingTaskId, setUpdatingTaskId] = useState<number | null>(null);
|
|
const [createTaskModalOpen, setCreateTaskModalOpen] = useState(false);
|
|
const [createTaskError, setCreateTaskError] = useState<string | null>(null);
|
|
const [newTaskName, setNewTaskName] = useState('');
|
|
const [newTaskDescription, setNewTaskDescription] = useState('');
|
|
const [showCompletedTasks, setShowCompletedTasks] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const validProjectId = useMemo(() => Number.isFinite(projectId) && projectId > 0, [projectId]);
|
|
const currentUserId = useMemo(() => (user?.id ? String(user.id) : null), [user]);
|
|
const infoRows = useMemo(() => {
|
|
if (!project) return [];
|
|
|
|
return [
|
|
{ label: 'Projektname', value: project.name || '-' },
|
|
{ label: 'Projekt-Nr.', value: String(project.projectNumber || '-') },
|
|
{ label: 'Aktive Phase', value: String(project.active_phase || '-') },
|
|
{ label: 'Kunde', value: getRefName(project.customer) },
|
|
{ label: 'Objekt', value: getRefName(project.plant) },
|
|
{ label: 'Vertrag', value: getRefName(project.contract) },
|
|
{ label: 'Projekttyp', value: getRefName(project.projecttype || project.projectType) },
|
|
{ label: 'Kunden-Ref.', value: String(project.customerRef || '-') },
|
|
{ label: 'Leistung', value: String(project.measure || '-') },
|
|
{ label: 'Erstellt', value: formatDateTime(project.createdAt || project.created_at) },
|
|
{ label: 'Aktualisiert', value: formatDateTime(project.updatedAt || project.updated_at) },
|
|
];
|
|
}, [project]);
|
|
const visibleTasks = useMemo(() => {
|
|
return (tasks || [])
|
|
.filter((task) => {
|
|
const status = normalizeTaskStatus(task.categorie);
|
|
return showCompletedTasks || status !== 'Abgeschlossen';
|
|
})
|
|
.sort((a, b) => {
|
|
const statusDiff =
|
|
TASK_STATUS_ORDER.indexOf(normalizeTaskStatus(a.categorie)) -
|
|
TASK_STATUS_ORDER.indexOf(normalizeTaskStatus(b.categorie));
|
|
if (statusDiff !== 0) return statusDiff;
|
|
return Number(b.id) - Number(a.id);
|
|
});
|
|
}, [showCompletedTasks, tasks]);
|
|
|
|
const load = useCallback(async (showSpinner = true) => {
|
|
if (!token || !validProjectId) return;
|
|
if (showSpinner) setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const [projectData, fileData, taskData] = await Promise.all([
|
|
fetchProjectById(token, projectId),
|
|
fetchProjectFiles(token, projectId),
|
|
fetchProjectTasks(token, projectId),
|
|
]);
|
|
setProject(projectData);
|
|
setFiles(fileData);
|
|
setTasks(taskData);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Projektdaten konnten nicht geladen werden.');
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, [projectId, token, validProjectId]);
|
|
|
|
useEffect(() => {
|
|
if (!token || !validProjectId) return;
|
|
void load(true);
|
|
}, [load, token, validProjectId]);
|
|
|
|
async function onRefresh() {
|
|
setRefreshing(true);
|
|
await load(false);
|
|
}
|
|
|
|
function closeCreateTaskModal() {
|
|
setCreateTaskModalOpen(false);
|
|
setCreateTaskError(null);
|
|
setNewTaskName('');
|
|
setNewTaskDescription('');
|
|
}
|
|
|
|
async function onCreateTask() {
|
|
if (!token || !validProjectId || creatingTask) return;
|
|
|
|
const name = newTaskName.trim();
|
|
if (!name) {
|
|
setCreateTaskError('Bitte einen Aufgabentitel eingeben.');
|
|
return;
|
|
}
|
|
|
|
setCreatingTask(true);
|
|
setCreateTaskError(null);
|
|
setError(null);
|
|
|
|
try {
|
|
await createProjectTask(token, {
|
|
projectId,
|
|
name,
|
|
description: newTaskDescription.trim() || null,
|
|
userId: currentUserId,
|
|
});
|
|
closeCreateTaskModal();
|
|
await load(false);
|
|
} catch (err) {
|
|
setCreateTaskError(err instanceof Error ? err.message : 'Aufgabe konnte nicht erstellt werden.');
|
|
} finally {
|
|
setCreatingTask(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 onCompleteTask(task: Task) {
|
|
if (!token || !task?.id) return;
|
|
if (normalizeTaskStatus(task.categorie) === 'Abgeschlossen') return;
|
|
|
|
setUpdatingTaskId(Number(task.id));
|
|
setError(null);
|
|
|
|
try {
|
|
await updateTask(token, Number(task.id), { categorie: 'Abgeschlossen' });
|
|
setTasks((prev) =>
|
|
prev.map((item) => (Number(item.id) === Number(task.id) ? { ...item, categorie: 'Abgeschlossen' } : item))
|
|
);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Aufgabe konnte nicht abgeschlossen werden.');
|
|
} finally {
|
|
setUpdatingTaskId(null);
|
|
}
|
|
}
|
|
|
|
async function onPickAndUpload() {
|
|
if (!token || !validProjectId || 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 uploadProjectFile(token, {
|
|
projectId,
|
|
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 || !validProjectId) return;
|
|
|
|
setUploading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
await uploadProjectFile(token, {
|
|
projectId,
|
|
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 || !validProjectId || 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 || !validProjectId || 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');
|
|
}
|
|
|
|
if (!validProjectId) {
|
|
return (
|
|
<View style={styles.centered}>
|
|
<Text style={styles.error}>Ungültige Projekt-ID.</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
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}>Projekt wird geladen...</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
{!loading && project ? (
|
|
<>
|
|
<View style={styles.card}>
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.title}>Informationen</Text>
|
|
<Pressable
|
|
style={styles.smallPrimaryButton}
|
|
onPress={() =>
|
|
router.push({
|
|
pathname: '/more/wiki',
|
|
params: {
|
|
entityType: 'projects',
|
|
entityId: String(projectId),
|
|
title: `Projekt-Wiki: ${String(project.name || projectId)}`,
|
|
},
|
|
})
|
|
}>
|
|
<Text style={styles.smallPrimaryButtonText}>Wiki</Text>
|
|
</Pressable>
|
|
</View>
|
|
<View style={styles.infoTable}>
|
|
{infoRows.map((row) => (
|
|
<View key={row.label} style={styles.infoRow}>
|
|
<Text style={styles.infoLabel}>{row.label}</Text>
|
|
<Text style={styles.infoValue}>{row.value}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
{project.notes ? (
|
|
<View style={styles.notesBox}>
|
|
<Text style={styles.notesLabel}>Notizen</Text>
|
|
<Text style={styles.notes}>{String(project.notes)}</Text>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
|
|
<View style={styles.card}>
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>Aufgaben ({tasks.length})</Text>
|
|
<View style={styles.sectionHeaderActions}>
|
|
<Pressable
|
|
style={[styles.filterChip, showCompletedTasks ? styles.filterChipActive : null]}
|
|
onPress={() => setShowCompletedTasks((prev) => !prev)}>
|
|
<Text style={[styles.filterChipText, showCompletedTasks ? styles.filterChipTextActive : null]}>
|
|
Abgeschlossene anzeigen
|
|
</Text>
|
|
</Pressable>
|
|
<Pressable style={styles.smallPrimaryButton} onPress={() => setCreateTaskModalOpen(true)}>
|
|
<Text style={styles.smallPrimaryButtonText}>Neue Aufgabe</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
{visibleTasks.length === 0 ? (
|
|
<Text style={styles.empty}>Keine Aufgaben vorhanden.</Text>
|
|
) : (
|
|
visibleTasks.map((task) => {
|
|
const status = normalizeTaskStatus(task.categorie);
|
|
const isUpdatingTask = updatingTaskId === Number(task.id);
|
|
const canComplete = status !== 'Abgeschlossen';
|
|
return (
|
|
<View key={String(task.id)} style={styles.taskRow}>
|
|
<View style={styles.taskRowMain}>
|
|
<Text style={styles.taskTitle} numberOfLines={2}>
|
|
{task.name || '-'}
|
|
</Text>
|
|
{task.description ? (
|
|
<Text style={styles.taskDescription} numberOfLines={3}>
|
|
{String(task.description)}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
<View style={styles.taskRowFooter}>
|
|
<Text style={styles.statusBadge}>{status}</Text>
|
|
{canComplete ? (
|
|
<Pressable
|
|
style={[styles.completeButton, isUpdatingTask ? styles.buttonDisabled : null]}
|
|
onPress={() => onCompleteTask(task)}
|
|
disabled={isUpdatingTask}>
|
|
<Text style={styles.completeButtonText}>{isUpdatingTask ? '...' : 'Abhaken'}</Text>
|
|
</Pressable>
|
|
) : null}
|
|
</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}
|
|
|
|
<Modal visible={createTaskModalOpen} transparent animationType="fade" onRequestClose={closeCreateTaskModal}>
|
|
<View style={styles.modalOverlay}>
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
style={styles.modalKeyboardWrap}>
|
|
<View style={styles.modalCard}>
|
|
<Text style={styles.modalTitle}>Neue Projekt-Aufgabe</Text>
|
|
|
|
<TextInput
|
|
placeholder="Titel"
|
|
placeholderTextColor="#9ca3af"
|
|
style={styles.input}
|
|
value={newTaskName}
|
|
onChangeText={setNewTaskName}
|
|
/>
|
|
<TextInput
|
|
placeholder="Beschreibung (optional)"
|
|
placeholderTextColor="#9ca3af"
|
|
style={[styles.input, styles.inputMultiline]}
|
|
multiline
|
|
value={newTaskDescription}
|
|
onChangeText={setNewTaskDescription}
|
|
/>
|
|
|
|
{createTaskError ? <Text style={styles.error}>{createTaskError}</Text> : null}
|
|
|
|
<View style={styles.modalActions}>
|
|
<Pressable
|
|
style={[styles.secondaryButton, creatingTask ? styles.buttonDisabled : null]}
|
|
onPress={closeCreateTaskModal}
|
|
disabled={creatingTask}>
|
|
<Text style={styles.secondaryButtonText}>Abbrechen</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={[styles.primaryButton, creatingTask ? styles.buttonDisabled : null]}
|
|
onPress={onCreateTask}
|
|
disabled={creatingTask}>
|
|
<Text style={styles.primaryButtonText}>{creatingTask ? 'Speichere...' : 'Anlegen'}</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
</View>
|
|
</Modal>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
padding: 16,
|
|
gap: 12,
|
|
backgroundColor: '#f9fafb',
|
|
},
|
|
centered: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: 16,
|
|
},
|
|
card: {
|
|
backgroundColor: '#ffffff',
|
|
borderRadius: 12,
|
|
borderWidth: 1,
|
|
borderColor: '#e5e7eb',
|
|
padding: 12,
|
|
gap: 8,
|
|
},
|
|
title: {
|
|
color: '#111827',
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
},
|
|
infoTable: {
|
|
borderWidth: 1,
|
|
borderColor: '#e5e7eb',
|
|
borderRadius: 10,
|
|
overflow: 'hidden',
|
|
},
|
|
infoRow: {
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#e5e7eb',
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 8,
|
|
gap: 2,
|
|
backgroundColor: '#ffffff',
|
|
},
|
|
infoLabel: {
|
|
color: '#6b7280',
|
|
fontSize: 12,
|
|
textTransform: 'uppercase',
|
|
fontWeight: '600',
|
|
},
|
|
infoValue: {
|
|
color: '#111827',
|
|
fontSize: 14,
|
|
fontWeight: '500',
|
|
},
|
|
notesBox: {
|
|
borderWidth: 1,
|
|
borderColor: '#e5e7eb',
|
|
borderRadius: 10,
|
|
padding: 10,
|
|
backgroundColor: '#fafafa',
|
|
gap: 4,
|
|
},
|
|
notesLabel: {
|
|
color: '#6b7280',
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
textTransform: 'uppercase',
|
|
},
|
|
notes: {
|
|
color: '#374151',
|
|
fontSize: 14,
|
|
},
|
|
sectionHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
gap: 10,
|
|
},
|
|
sectionTitle: {
|
|
color: '#111827',
|
|
fontSize: 16,
|
|
fontWeight: '700',
|
|
},
|
|
sectionHeaderActions: {
|
|
alignItems: 'flex-end',
|
|
gap: 8,
|
|
},
|
|
filterChip: {
|
|
borderRadius: 999,
|
|
borderWidth: 1,
|
|
borderColor: '#d1d5db',
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 6,
|
|
backgroundColor: '#ffffff',
|
|
},
|
|
filterChipActive: {
|
|
borderColor: PRIMARY,
|
|
backgroundColor: '#eff9ea',
|
|
},
|
|
filterChipText: {
|
|
color: '#374151',
|
|
fontSize: 12,
|
|
fontWeight: '500',
|
|
},
|
|
filterChipTextActive: {
|
|
color: '#3d7a30',
|
|
},
|
|
smallPrimaryButton: {
|
|
minHeight: 34,
|
|
borderRadius: 8,
|
|
backgroundColor: PRIMARY,
|
|
paddingHorizontal: 11,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
smallPrimaryButtonText: {
|
|
color: '#ffffff',
|
|
fontWeight: '700',
|
|
fontSize: 12,
|
|
},
|
|
taskRow: {
|
|
borderWidth: 1,
|
|
borderColor: '#e5e7eb',
|
|
borderRadius: 10,
|
|
padding: 10,
|
|
gap: 8,
|
|
},
|
|
taskRowMain: {
|
|
gap: 4,
|
|
},
|
|
taskTitle: {
|
|
color: '#111827',
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
},
|
|
taskDescription: {
|
|
color: '#374151',
|
|
fontSize: 13,
|
|
},
|
|
statusBadge: {
|
|
alignSelf: 'flex-start',
|
|
color: '#3d7a30',
|
|
backgroundColor: '#eff9ea',
|
|
borderRadius: 999,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
fontSize: 12,
|
|
overflow: 'hidden',
|
|
},
|
|
taskRowFooter: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
},
|
|
completeButton: {
|
|
minHeight: 28,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: PRIMARY,
|
|
backgroundColor: '#eff9ea',
|
|
paddingHorizontal: 10,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
completeButtonText: {
|
|
color: '#3d7a30',
|
|
fontSize: 12,
|
|
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,
|
|
},
|
|
modalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
justifyContent: 'center',
|
|
padding: 16,
|
|
},
|
|
modalKeyboardWrap: {
|
|
width: '100%',
|
|
},
|
|
modalCard: {
|
|
backgroundColor: '#ffffff',
|
|
borderRadius: 14,
|
|
padding: 14,
|
|
gap: 10,
|
|
},
|
|
modalTitle: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
color: '#111827',
|
|
},
|
|
input: {
|
|
borderWidth: 1,
|
|
borderColor: '#d1d5db',
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 10,
|
|
fontSize: 15,
|
|
color: '#111827',
|
|
backgroundColor: '#ffffff',
|
|
},
|
|
inputMultiline: {
|
|
minHeight: 72,
|
|
textAlignVertical: 'top',
|
|
},
|
|
modalActions: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
gap: 8,
|
|
marginTop: 2,
|
|
},
|
|
secondaryButton: {
|
|
minHeight: 42,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 14,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: '#e5e7eb',
|
|
},
|
|
secondaryButtonText: {
|
|
color: '#111827',
|
|
fontWeight: '600',
|
|
},
|
|
primaryButton: {
|
|
minHeight: 42,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 14,
|
|
backgroundColor: PRIMARY,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
primaryButtonText: {
|
|
color: '#ffffff',
|
|
fontWeight: '600',
|
|
},
|
|
buttonDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
});
|