Files
task-team/mobile/app/(tabs)/calendar.tsx
Admin db81100b5b React Native Expo app — full mobile client
- Expo SDK 54, expo-router, NativeWind
- 7 screens: login, tasks, calendar, goals, chat, settings, tabs
- API client (api.hasdo.info), SecureStore auth, AuthContext
- Tab navigation: Ukoly, Kalendar, Cile, Chat, Nastaveni
- Pull-to-refresh, FAB, group colors, priority dots
- Calendar grid with task dots
- AI chat with keyboard avoiding
- Web export verified (780 modules, 1.5MB bundle)
- Bundle ID: info.hasdo.taskteam

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:09:35 +00:00

265 lines
7.2 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
RefreshControl,
TouchableOpacity,
Alert,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useAuthContext } from '../../lib/AuthContext';
import * as api from '../../lib/api';
const DAYS = ['Po', 'Ut', 'St', 'Ct', 'Pa', 'So', 'Ne'];
const MONTHS = [
'Leden', 'Unor', 'Brezen', 'Duben', 'Kveten', 'Cerven',
'Cervenec', 'Srpen', 'Zari', 'Rijen', 'Listopad', 'Prosinec',
];
function getDaysInMonth(year: number, month: number) {
return new Date(year, month + 1, 0).getDate();
}
function getFirstDayOfMonth(year: number, month: number) {
const day = new Date(year, month, 1).getDay();
return day === 0 ? 6 : day - 1; // Monday = 0
}
export default function CalendarScreen() {
const { token } = useAuthContext();
const [tasks, setTasks] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const loadTasks = useCallback(async () => {
if (!token) return;
try {
const res = await api.getTasks(token);
setTasks(res.data || []);
} catch (err: any) {
Alert.alert('Chyba', err.message);
}
}, [token]);
useEffect(() => {
loadTasks();
}, [loadTasks]);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await loadTasks();
setRefreshing(false);
}, [loadTasks]);
const prevMonth = () => {
setCurrentDate(new Date(year, month - 1, 1));
setSelectedDate(null);
};
const nextMonth = () => {
setCurrentDate(new Date(year, month + 1, 1));
setSelectedDate(null);
};
const daysInMonth = getDaysInMonth(year, month);
const firstDay = getFirstDayOfMonth(year, month);
const tasksByDate: Record<string, any[]> = {};
tasks.forEach((t) => {
if (t.due_date) {
const d = t.due_date.substring(0, 10);
if (!tasksByDate[d]) tasksByDate[d] = [];
tasksByDate[d].push(t);
}
});
const today = new Date().toISOString().substring(0, 10);
const selectedTasks = selectedDate ? tasksByDate[selectedDate] || [] : [];
return (
<ScrollView
style={styles.container}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{/* Month navigator */}
<View style={styles.monthNav}>
<TouchableOpacity onPress={prevMonth} style={styles.navBtn}>
<Ionicons name="chevron-back" size={24} color="#3B82F6" />
</TouchableOpacity>
<Text style={styles.monthTitle}>
{MONTHS[month]} {year}
</Text>
<TouchableOpacity onPress={nextMonth} style={styles.navBtn}>
<Ionicons name="chevron-forward" size={24} color="#3B82F6" />
</TouchableOpacity>
</View>
{/* Day headers */}
<View style={styles.dayHeaders}>
{DAYS.map((d) => (
<Text key={d} style={styles.dayHeader}>
{d}
</Text>
))}
</View>
{/* Calendar grid */}
<View style={styles.grid}>
{Array.from({ length: firstDay }).map((_, i) => (
<View key={`empty-${i}`} style={styles.dayCell} />
))}
{Array.from({ length: daysInMonth }).map((_, i) => {
const day = i + 1;
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const hasTasks = !!tasksByDate[dateStr];
const isToday = dateStr === today;
const isSelected = dateStr === selectedDate;
return (
<TouchableOpacity
key={day}
style={[
styles.dayCell,
isToday && styles.todayCell,
isSelected && styles.selectedCell,
]}
onPress={() => setSelectedDate(dateStr)}
>
<Text
style={[
styles.dayText,
isToday && styles.todayText,
isSelected && styles.selectedText,
]}
>
{day}
</Text>
{hasTasks && (
<View
style={[
styles.dot,
isSelected && { backgroundColor: '#fff' },
]}
/>
)}
</TouchableOpacity>
);
})}
</View>
{/* Selected date tasks */}
{selectedDate && (
<View style={styles.tasksSection}>
<Text style={styles.tasksSectionTitle}>
Ukoly na {selectedDate}
</Text>
{selectedTasks.length === 0 ? (
<Text style={styles.noTasks}>Zadne ukoly</Text>
) : (
selectedTasks.map((t) => (
<View key={t.id} style={styles.taskItem}>
<Ionicons
name={t.status === 'done' ? 'checkmark-circle' : 'ellipse-outline'}
size={20}
color={t.status === 'done' ? '#22C55E' : '#94A3B8'}
/>
<Text
style={[
styles.taskItemText,
t.status === 'done' && { textDecorationLine: 'line-through', color: '#94A3B8' },
]}
>
{t.title}
</Text>
</View>
))
)}
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F8FAFC' },
monthNav: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
backgroundColor: '#fff',
},
navBtn: { padding: 8 },
monthTitle: { fontSize: 18, fontWeight: '700', color: '#1E293B' },
dayHeaders: {
flexDirection: 'row',
backgroundColor: '#fff',
paddingBottom: 8,
borderBottomWidth: 1,
borderBottomColor: '#E2E8F0',
},
dayHeader: {
flex: 1,
textAlign: 'center',
fontSize: 12,
fontWeight: '600',
color: '#64748B',
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
backgroundColor: '#fff',
padding: 4,
},
dayCell: {
width: '14.28%',
aspectRatio: 1,
alignItems: 'center',
justifyContent: 'center',
},
todayCell: {
backgroundColor: '#EFF6FF',
borderRadius: 20,
},
selectedCell: {
backgroundColor: '#3B82F6',
borderRadius: 20,
},
dayText: { fontSize: 15, color: '#1E293B' },
todayText: { color: '#3B82F6', fontWeight: '700' },
selectedText: { color: '#fff', fontWeight: '700' },
dot: {
width: 5,
height: 5,
borderRadius: 3,
backgroundColor: '#3B82F6',
marginTop: 2,
},
tasksSection: {
margin: 12,
padding: 16,
backgroundColor: '#fff',
borderRadius: 12,
},
tasksSectionTitle: { fontSize: 16, fontWeight: '600', color: '#1E293B', marginBottom: 12 },
noTasks: { color: '#94A3B8', fontSize: 14 },
taskItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#F1F5F9',
},
taskItemText: { fontSize: 14, color: '#1E293B', flex: 1 },
});