KI-AGENT: Kalender, Terminbearbeitung und Dateivorschau verbessern
This commit is contained in:
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
|
||||
import { fetchStaffTimeSpans, fetchTasks, Task } from '@/src/lib/api';
|
||||
import { EventCreateModal } from '@/components/event-create-modal';
|
||||
import { CalendarEvent, fetchEvents, fetchProjects, fetchStaffTimeSpans, fetchTasks, Project, Task } from '@/src/lib/api';
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
|
||||
const PRIMARY = '#69c350';
|
||||
@@ -38,12 +39,25 @@ function formatDateTime(value: string | null): string {
|
||||
});
|
||||
}
|
||||
|
||||
function dateKey(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function formatEventTime(value: string): string {
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
export default function DashboardScreen() {
|
||||
const { token, user } = useAuth();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [createEventOpen, setCreateEventOpen] = useState(false);
|
||||
const [selectedCalendarDate, setSelectedCalendarDate] = useState(() => new Date());
|
||||
const [data, setData] = useState<DashboardData>({
|
||||
tasks: [],
|
||||
openTasks: 0,
|
||||
@@ -62,9 +76,11 @@ export default function DashboardScreen() {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [taskRows, spans] = await Promise.all([
|
||||
const [taskRows, spans, eventRows, projectRows] = await Promise.all([
|
||||
fetchTasks(token),
|
||||
fetchStaffTimeSpans(token, currentUserId),
|
||||
fetchEvents(token),
|
||||
fetchProjects(token),
|
||||
]);
|
||||
|
||||
const tasks = taskRows || [];
|
||||
@@ -92,6 +108,8 @@ export default function DashboardScreen() {
|
||||
pendingSubmissions,
|
||||
todayMinutes,
|
||||
});
|
||||
setEvents(eventRows);
|
||||
setProjects(projectRows);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Dashboard konnte nicht geladen werden.');
|
||||
} finally {
|
||||
@@ -112,6 +130,26 @@ export default function DashboardScreen() {
|
||||
await loadDashboard(false);
|
||||
}
|
||||
|
||||
const calendarDays = useMemo(() => {
|
||||
const start = new Date();
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return Array.from({ length: 7 }, (_, index) => {
|
||||
const date = new Date(start);
|
||||
date.setDate(start.getDate() + index);
|
||||
return date;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectedEvents = useMemo(() => {
|
||||
const selectedKey = dateKey(selectedCalendarDate);
|
||||
return events
|
||||
.filter((event) => {
|
||||
const start = new Date(event.startDate);
|
||||
return !Number.isNaN(start.getTime()) && dateKey(start) === selectedKey;
|
||||
})
|
||||
.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
|
||||
}, [events, selectedCalendarDate]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.container}
|
||||
@@ -158,6 +196,60 @@ export default function DashboardScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.calendarCard}>
|
||||
<View style={styles.calendarHeader}>
|
||||
<View>
|
||||
<Text style={styles.calendarTitle}>Kalender</Text>
|
||||
<Text style={styles.calendarSubtitle}>
|
||||
{selectedCalendarDate.toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long' })}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => router.push('/more/calendar' as any)}>
|
||||
<Text style={styles.calendarLink}>Monat öffnen</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.calendarDays}>
|
||||
{calendarDays.map((date) => {
|
||||
const key = dateKey(date);
|
||||
const selected = key === dateKey(selectedCalendarDate);
|
||||
const count = events.filter((event) => {
|
||||
const start = new Date(event.startDate);
|
||||
return !Number.isNaN(start.getTime()) && dateKey(start) === key;
|
||||
}).length;
|
||||
return (
|
||||
<Pressable
|
||||
key={key}
|
||||
style={[styles.calendarDay, selected ? styles.calendarDaySelected : null]}
|
||||
onPress={() => setSelectedCalendarDate(date)}>
|
||||
<Text style={[styles.calendarWeekday, selected ? styles.calendarDayTextSelected : null]}>
|
||||
{date.toLocaleDateString('de-DE', { weekday: 'short' }).replace('.', '')}
|
||||
</Text>
|
||||
<Text style={[styles.calendarDayNumber, selected ? styles.calendarDayTextSelected : null]}>{date.getDate()}</Text>
|
||||
<View style={[styles.calendarDot, count > 0 ? styles.calendarDotActive : null]} />
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{selectedEvents.length === 0 ? <Text style={styles.calendarEmpty}>Keine Termine an diesem Tag.</Text> : null}
|
||||
{selectedEvents.slice(0, 3).map((event) => (
|
||||
<Pressable
|
||||
key={String(event.id)}
|
||||
style={styles.calendarEvent}
|
||||
onPress={() => router.push(`/more/event/${event.id}` as any)}>
|
||||
<View style={[styles.calendarEventMark, { backgroundColor: event.color || PRIMARY }]} />
|
||||
<Text style={styles.calendarEventTime}>{formatEventTime(event.startDate)}</Text>
|
||||
<Text style={styles.calendarEventName} numberOfLines={1}>{event.name}</Text>
|
||||
{event.state === 'Entwurf' ? <Text style={styles.calendarEventDraft}>Entwurf</Text> : null}
|
||||
<Text style={styles.calendarEventArrow}>›</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
<Pressable style={styles.calendarAddButton} onPress={() => setCreateEventOpen(true)}>
|
||||
<Text style={styles.calendarAddButtonText}>+ Termin eintragen</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.quickActionsCard}>
|
||||
<Text style={styles.quickActionsTitle}>Schnellzugriff</Text>
|
||||
<View style={styles.quickActionsRow}>
|
||||
@@ -175,6 +267,14 @@ export default function DashboardScreen() {
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<EventCreateModal
|
||||
visible={createEventOpen}
|
||||
projects={projects}
|
||||
initialDate={selectedCalendarDate}
|
||||
onClose={() => setCreateEventOpen(false)}
|
||||
onCreated={() => loadDashboard(false)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
@@ -232,6 +332,35 @@ const styles = StyleSheet.create({
|
||||
padding: 12,
|
||||
gap: 10,
|
||||
},
|
||||
calendarCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e7eb',
|
||||
padding: 12,
|
||||
gap: 10,
|
||||
},
|
||||
calendarHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 },
|
||||
calendarTitle: { color: '#111827', fontSize: 16, fontWeight: '700' },
|
||||
calendarSubtitle: { color: '#6b7280', fontSize: 12, marginTop: 2 },
|
||||
calendarLink: { color: '#3d7a30', fontSize: 12, fontWeight: '700' },
|
||||
calendarDays: { flexDirection: 'row', gap: 4 },
|
||||
calendarDay: { flex: 1, minHeight: 62, borderRadius: 9, alignItems: 'center', justifyContent: 'center', gap: 2 },
|
||||
calendarDaySelected: { backgroundColor: PRIMARY },
|
||||
calendarWeekday: { color: '#6b7280', fontSize: 10, fontWeight: '600', textTransform: 'uppercase' },
|
||||
calendarDayNumber: { color: '#111827', fontSize: 16, fontWeight: '700' },
|
||||
calendarDayTextSelected: { color: '#ffffff' },
|
||||
calendarDot: { width: 5, height: 5, borderRadius: 3, backgroundColor: 'transparent' },
|
||||
calendarDotActive: { backgroundColor: '#f59e0b' },
|
||||
calendarEmpty: { color: '#6b7280', fontSize: 13, paddingVertical: 4 },
|
||||
calendarEvent: { minHeight: 44, flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 9, overflow: 'hidden' },
|
||||
calendarEventMark: { width: 4, alignSelf: 'stretch' },
|
||||
calendarEventTime: { color: '#3d7a30', fontSize: 12, fontWeight: '700', paddingHorizontal: 8 },
|
||||
calendarEventName: { flex: 1, color: '#111827', fontSize: 13, fontWeight: '600' },
|
||||
calendarEventDraft: { color: '#92400e', backgroundColor: '#fef3c7', borderRadius: 999, paddingHorizontal: 7, paddingVertical: 3, fontSize: 10, fontWeight: '700' },
|
||||
calendarEventArrow: { color: '#9ca3af', fontSize: 21, paddingHorizontal: 7 },
|
||||
calendarAddButton: { minHeight: 40, borderRadius: 9, backgroundColor: PRIMARY, alignItems: 'center', justifyContent: 'center' },
|
||||
calendarAddButtonText: { color: '#ffffff', fontSize: 13, fontWeight: '700' },
|
||||
quickActionsTitle: {
|
||||
color: '#111827',
|
||||
fontSize: 15,
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from 'react-native';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import { BarcodeScanningResult, BarcodeType, CameraView, useCameraPermissions } from 'expo-camera';
|
||||
|
||||
import { HistorySection } from '@/components/history-section';
|
||||
@@ -32,7 +31,7 @@ import {
|
||||
uploadCustomerFile,
|
||||
} from '@/src/lib/api';
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
import { getOpenableFileUrl } from '@/src/lib/file-opening';
|
||||
import { openAuthenticatedFile } from '@/src/lib/file-opening';
|
||||
|
||||
const PRIMARY = '#69c350';
|
||||
|
||||
@@ -276,13 +275,13 @@ export default function CustomerDetailScreen() {
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
if (!token || !file.url) return;
|
||||
setError(null);
|
||||
try {
|
||||
await openAuthenticatedFile(file, token);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Das Dokument konnte nicht geöffnet werden.');
|
||||
}
|
||||
}
|
||||
|
||||
async function onPickAndUpload() {
|
||||
|
||||
@@ -3,8 +3,9 @@ import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, T
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
|
||||
import { EventCreateModal } from '@/components/event-create-modal';
|
||||
import { HistorySection } from '@/components/history-section';
|
||||
import { CalendarEvent, fetchEventById } from '@/src/lib/api';
|
||||
import { CalendarEvent, EventState, fetchEventById, fetchProjects, Project, updateEvent } from '@/src/lib/api';
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
|
||||
const PRIMARY = '#69c350';
|
||||
@@ -37,6 +38,9 @@ export default function EventDetailScreen() {
|
||||
const eventId = Number(params.id);
|
||||
const { token } = useAuth();
|
||||
const [event, setEvent] = useState<CalendarEvent | null>(null);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [updatingState, setUpdatingState] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -47,7 +51,12 @@ export default function EventDetailScreen() {
|
||||
if (showSpinner) setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
setEvent(await fetchEventById(token, eventId));
|
||||
const [eventRow, projectRows] = await Promise.all([
|
||||
fetchEventById(token, eventId),
|
||||
fetchProjects(token),
|
||||
]);
|
||||
setEvent(eventRow);
|
||||
setProjects(projectRows);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Der Termin konnte nicht geladen werden.');
|
||||
} finally {
|
||||
@@ -72,13 +81,26 @@ export default function EventDetailScreen() {
|
||||
await WebBrowser.openBrowserAsync(url, { controlsColor: PRIMARY, showTitle: true });
|
||||
}
|
||||
|
||||
async function changeState(nextState: EventState) {
|
||||
if (!token || !event || updatingState || event.state === nextState) return;
|
||||
setUpdatingState(true);
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await updateEvent(token, eventId, { state: nextState });
|
||||
setEvent((current) => current ? { ...current, state: updated.state } : updated);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Der Status konnte nicht geändert werden.');
|
||||
} finally {
|
||||
setUpdatingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
const rows = event
|
||||
? [
|
||||
{ label: 'Beginn', value: formatDateTime(event.startDate) },
|
||||
{ label: 'Ende', value: formatDateTime(event.endDate) },
|
||||
{ label: 'Projekt', value: relationName(event.project) },
|
||||
{ label: 'Kunde', value: relationName(event.customer) },
|
||||
{ label: 'Status', value: String(event.state || '-') },
|
||||
{ label: 'Wiederholung', value: String(event.repeatInterval || '-') },
|
||||
]
|
||||
: [];
|
||||
@@ -97,6 +119,25 @@ export default function EventDetailScreen() {
|
||||
<View style={styles.headingRow}>
|
||||
<View style={[styles.colorMark, { backgroundColor: event.color || PRIMARY }]} />
|
||||
<Text style={styles.title}>{event.name}</Text>
|
||||
<Pressable style={styles.editButton} onPress={() => setEditOpen(true)}>
|
||||
<Text style={styles.editButtonText}>Bearbeiten</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.statusSection}>
|
||||
<Text style={styles.statusLabel}>Status</Text>
|
||||
<View style={styles.statusRow}>
|
||||
{(['Entwurf', 'Final'] as EventState[]).map((option) => (
|
||||
<Pressable
|
||||
key={option}
|
||||
disabled={updatingState}
|
||||
style={[styles.statusButton, event.state === option ? styles.statusButtonActive : null]}
|
||||
onPress={() => changeState(option)}>
|
||||
<Text style={[styles.statusButtonText, event.state === option ? styles.statusButtonTextActive : null]}>
|
||||
{option}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.table}>
|
||||
{rows.map((row) => (
|
||||
@@ -110,6 +151,13 @@ export default function EventDetailScreen() {
|
||||
{event.link ? <Pressable style={styles.linkButton} onPress={openLink}><Text style={styles.linkButtonText}>Link öffnen</Text></Pressable> : null}
|
||||
</View>
|
||||
<HistorySection resource="events" resourceId={eventId} />
|
||||
<EventCreateModal
|
||||
visible={editOpen}
|
||||
event={event}
|
||||
projects={projects}
|
||||
onClose={() => setEditOpen(false)}
|
||||
onUpdated={() => load(false)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
@@ -122,6 +170,15 @@ const styles = StyleSheet.create({
|
||||
headingRow: { flexDirection: 'row', alignItems: 'center', gap: 9 },
|
||||
colorMark: { width: 6, height: 28, borderRadius: 3 },
|
||||
title: { flex: 1, color: '#111827', fontSize: 19, fontWeight: '700' },
|
||||
editButton: { borderRadius: 8, backgroundColor: PRIMARY, paddingHorizontal: 10, paddingVertical: 7 },
|
||||
editButtonText: { color: '#ffffff', fontSize: 12, fontWeight: '700' },
|
||||
statusSection: { gap: 6 },
|
||||
statusLabel: { color: '#6b7280', fontSize: 12, textTransform: 'uppercase', fontWeight: '600' },
|
||||
statusRow: { flexDirection: 'row', gap: 8 },
|
||||
statusButton: { flex: 1, minHeight: 40, borderRadius: 9, borderWidth: 1, borderColor: '#d1d5db', alignItems: 'center', justifyContent: 'center' },
|
||||
statusButtonActive: { borderColor: PRIMARY, backgroundColor: '#eff9ea' },
|
||||
statusButtonText: { color: '#6b7280', fontSize: 13, fontWeight: '700' },
|
||||
statusButtonTextActive: { color: '#3d7a30' },
|
||||
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' },
|
||||
|
||||
@@ -134,6 +134,7 @@ export default function EventsScreen() {
|
||||
{projectName(event) ? <Text style={styles.eventProject}>{projectName(event)}</Text> : null}
|
||||
{event.notes ? <Text style={styles.eventNotes} numberOfLines={2}>{String(event.notes)}</Text> : null}
|
||||
</View>
|
||||
{event.state === 'Entwurf' ? <Text style={styles.eventDraft}>Entwurf</Text> : null}
|
||||
<Text style={styles.arrow}>›</Text>
|
||||
</Pressable>
|
||||
))
|
||||
@@ -183,6 +184,7 @@ const styles = StyleSheet.create({
|
||||
eventDate: { color: '#374151', fontSize: 13 },
|
||||
eventProject: { color: '#3d7a30', fontSize: 12, fontWeight: '600' },
|
||||
eventNotes: { color: '#6b7280', fontSize: 12 },
|
||||
eventDraft: { alignSelf: 'center', color: '#92400e', backgroundColor: '#fef3c7', borderRadius: 999, paddingHorizontal: 8, paddingVertical: 4, fontSize: 10, fontWeight: '700' },
|
||||
arrow: { alignSelf: 'center', color: '#9ca3af', fontSize: 25, paddingRight: 12 },
|
||||
loadingBox: { paddingVertical: 24, alignItems: 'center', gap: 8 },
|
||||
loadingText: { color: '#6b7280' },
|
||||
|
||||
@@ -3,12 +3,11 @@ import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, T
|
||||
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 { HistorySection } from '@/components/history-section';
|
||||
import { fetchPlantById, fetchPlantFiles, Plant, ProjectFile, uploadPlantFile } from '@/src/lib/api';
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
import { getOpenableFileUrl } from '@/src/lib/file-opening';
|
||||
import { openAuthenticatedFile } from '@/src/lib/file-opening';
|
||||
|
||||
const PRIMARY = '#69c350';
|
||||
|
||||
@@ -90,13 +89,13 @@ export default function PlantDetailScreen() {
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
if (!token || !file.url) return;
|
||||
setError(null);
|
||||
try {
|
||||
await openAuthenticatedFile(file, token);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Das Dokument konnte nicht geöffnet werden.');
|
||||
}
|
||||
}
|
||||
|
||||
async function onPickAndUpload() {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
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';
|
||||
@@ -34,7 +33,7 @@ import {
|
||||
updateTask,
|
||||
} from '@/src/lib/api';
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
import { getOpenableFileUrl } from '@/src/lib/file-opening';
|
||||
import { openAuthenticatedFile } from '@/src/lib/file-opening';
|
||||
|
||||
const PRIMARY = '#69c350';
|
||||
const TASK_STATUS_ORDER: TaskStatus[] = ['Offen', 'In Bearbeitung', 'Abgeschlossen'];
|
||||
@@ -193,13 +192,13 @@ export default function ProjectDetailScreen() {
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
if (!token || !file.url) return;
|
||||
setError(null);
|
||||
try {
|
||||
await openAuthenticatedFile(file, token);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Das Dokument konnte nicht geöffnet werden.');
|
||||
}
|
||||
}
|
||||
|
||||
async function onCompleteTask(task: Task) {
|
||||
|
||||
Reference in New Issue
Block a user