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