883 lines
26 KiB
TypeScript
883 lines
26 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 { EventCreateModal } from '@/components/event-create-modal';
|
||
import { HistorySection } from '@/components/history-section';
|
||
import {
|
||
CalendarEvent,
|
||
createProjectTask,
|
||
fetchProjectById,
|
||
fetchProjectEvents,
|
||
fetchProjectFiles,
|
||
fetchProjectTasks,
|
||
Project,
|
||
ProjectFile,
|
||
Task,
|
||
TaskStatus,
|
||
uploadProjectFile,
|
||
updateTask,
|
||
} from '@/src/lib/api';
|
||
import { useAuth } from '@/src/providers/auth-provider';
|
||
import { getOpenableFileUrl } from '@/src/lib/file-opening';
|
||
|
||
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 [events, setEvents] = useState<CalendarEvent[]>([]);
|
||
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 [createEventModalOpen, setCreateEventModalOpen] = 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, eventData] = await Promise.all([
|
||
fetchProjectById(token, projectId),
|
||
fetchProjectFiles(token, projectId),
|
||
fetchProjectTasks(token, projectId),
|
||
fetchProjectEvents(token, projectId),
|
||
]);
|
||
setProject(projectData);
|
||
setFiles(fileData);
|
||
setTasks(taskData);
|
||
setEvents(eventData.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()));
|
||
} 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(getOpenableFileUrl(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>
|
||
|
||
<HistorySection resource="projects" resourceId={projectId} />
|
||
|
||
<View style={styles.card}>
|
||
<View style={styles.sectionHeader}>
|
||
<Text style={styles.sectionTitle}>Termine ({events.length})</Text>
|
||
<Pressable style={styles.smallPrimaryButton} onPress={() => setCreateEventModalOpen(true)}>
|
||
<Text style={styles.smallPrimaryButtonText}>Neuer Termin</Text>
|
||
</Pressable>
|
||
</View>
|
||
{events.length === 0 ? (
|
||
<Text style={styles.empty}>Keine Termine für dieses Projekt vorhanden.</Text>
|
||
) : (
|
||
events.map((event) => (
|
||
<Pressable
|
||
key={String(event.id)}
|
||
style={styles.eventRow}
|
||
onPress={() => router.push(`/more/event/${event.id}` as any)}>
|
||
<View style={[styles.eventColor, { backgroundColor: event.color || PRIMARY }]} />
|
||
<View style={styles.eventMain}>
|
||
<Text style={styles.eventName}>{event.name}</Text>
|
||
<Text style={styles.eventDate}>
|
||
{formatDateTime(event.startDate)}
|
||
{event.endDate ? ` – ${formatDateTime(event.endDate)}` : ''}
|
||
</Text>
|
||
</View>
|
||
<Text style={styles.eventArrow}>›</Text>
|
||
</Pressable>
|
||
))
|
||
)}
|
||
</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>
|
||
|
||
<EventCreateModal
|
||
visible={createEventModalOpen}
|
||
projects={project ? [project] : []}
|
||
initialProjectId={projectId}
|
||
onClose={() => setCreateEventModalOpen(false)}
|
||
onCreated={() => load(false)}
|
||
/>
|
||
</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',
|
||
},
|
||
eventRow: {
|
||
minHeight: 60,
|
||
flexDirection: 'row',
|
||
alignItems: 'stretch',
|
||
borderWidth: 1,
|
||
borderColor: '#e5e7eb',
|
||
borderRadius: 9,
|
||
overflow: 'hidden',
|
||
},
|
||
eventColor: {
|
||
width: 5,
|
||
},
|
||
eventMain: {
|
||
flex: 1,
|
||
padding: 9,
|
||
gap: 2,
|
||
},
|
||
eventName: {
|
||
color: '#111827',
|
||
fontSize: 14,
|
||
fontWeight: '700',
|
||
},
|
||
eventDate: {
|
||
color: '#6b7280',
|
||
fontSize: 12,
|
||
},
|
||
eventArrow: {
|
||
alignSelf: 'center',
|
||
color: '#9ca3af',
|
||
fontSize: 24,
|
||
paddingRight: 9,
|
||
},
|
||
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,
|
||
},
|
||
});
|