diff --git a/mobile/src/navigation/Navigation.tsx b/mobile/src/navigation/Navigation.tsx
index 07ca821..f13d2b6 100644
--- a/mobile/src/navigation/Navigation.tsx
+++ b/mobile/src/navigation/Navigation.tsx
@@ -20,6 +20,7 @@ import VisitsScreen from '@screens/VisitsScreen';
import MedicationsScreen from '@screens/MedicationsScreen';
import VaccinationsScreen from '../screens/VaccinationsScreen';
import WeightManagementScreen from '../screens/WeightManagementScreen';
+import CalendarScreen from '@screens/CalendarScreen';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
@@ -236,6 +237,14 @@ export default function Navigation() {
headerBackTitle: 'Takaisin',
}}
/>
+
>
) : (
<>
diff --git a/mobile/src/screens/CalendarScreen.tsx b/mobile/src/screens/CalendarScreen.tsx
new file mode 100644
index 0000000..49d102a
--- /dev/null
+++ b/mobile/src/screens/CalendarScreen.tsx
@@ -0,0 +1,663 @@
+import React, { useState, useEffect } from 'react';
+import { View, ScrollView, TouchableOpacity } from 'react-native';
+import { Text, Chip, ActivityIndicator, FAB, Portal, Modal, Button, TextInput } from 'react-native-paper';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import DateTimePicker from '@react-native-community/datetimepicker';
+import { calendarStyles as styles } from '../styles/screenStyles';
+import { COLORS, SPACING } from '../styles/theme';
+import apiClient from '../services/api';
+import calendarService from '../services/calendarService';
+import { visitsService } from '../services/visitsService';
+import { Pet, CalendarEvent } from '../types';
+
+
+const EVENT_TYPE_ICONS: Record = {
+ vaccination: 'needle',
+ veterinary: 'hospital-box',
+ medication: 'pill',
+ grooming: 'content-cut',
+ other: 'calendar-star'
+};
+
+const EVENT_TYPE_COLORS = {
+ vaccination: '#4CAF50',
+ veterinary: '#2196F3',
+ medication: '#FF9800',
+ grooming: '#9C27B0',
+ other: '#607D8B'
+};
+
+const DAYS_OF_WEEK = ['Ma', 'Ti', 'Ke', 'To', 'Pe', 'La', 'Su'];
+const MONTHS = [
+ 'Tammikuu', 'Helmikuu', 'Maaliskuu', 'Huhtikuu', 'Toukokuu', 'Kesäkuu',
+ 'Heinäkuu', 'Elokuu', 'Syyskuu', 'Lokakuu', 'Marraskuu', 'Joulukuu'
+];
+
+export default function CalendarScreen() {
+ const [pets, setPets] = useState([]);
+ const [selectedPetId, setSelectedPetId] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [events, setEvents] = useState([]);
+ const [visitTypes, setVisitTypes] = useState([]);
+ const [selectedTypeId, setSelectedTypeId] = useState(null);
+
+ // Calendar navigation state
+ const currentDate = new Date();
+ const [selectedMonth, setSelectedMonth] = useState(currentDate.getMonth());
+ const [selectedYear, setSelectedYear] = useState(currentDate.getFullYear());
+
+ // Modal state
+ const [modalVisible, setModalVisible] = useState(false);
+ const [saving, setSaving] = useState(false);
+ const [showDatePicker, setShowDatePicker] = useState(false);
+
+ // Form state
+ const [eventTitle, setEventTitle] = useState('');
+ const [eventDescription, setEventDescription] = useState('');
+ const [eventDate, setEventDate] = useState(new Date());
+
+ // Fetch pets and events from the API
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const petsResponse = await apiClient.get('/api/pets');
+
+ if (petsResponse.data.success && petsResponse.data.data) {
+ const fetchedPets = petsResponse.data.data;
+ setPets(fetchedPets);
+
+ // Set the first pet as selected by default
+ if (fetchedPets.length > 0) {
+ setSelectedPetId(fetchedPets[0].id);
+ }
+ }
+
+ // Try to fetch events from API
+ try {
+ const fetchedEvents = await calendarService.getAllEvents();
+ setEvents(fetchedEvents);
+ } catch (eventsError) {
+ console.log('Events API not available yet, starting with empty list');
+ setEvents([]);
+ }
+
+ // Fetch visit types
+ try {
+ const types = await visitsService.getVisitTypes();
+ console.log('Visit types fetched:', types);
+ setVisitTypes(types);
+ } catch (typesError) {
+ console.error('Failed to fetch visit types:', typesError);
+ setVisitTypes([]);
+ }
+
+ } catch (err: any) {
+ console.error('Failed to fetch data:', err);
+ setError('Tietojen lataus epäonnistui. Yritä uudelleen.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ // Get available years from events
+ const availableYears = Array.from(
+ new Set([
+ currentDate.getFullYear() - 1,
+ currentDate.getFullYear(),
+ currentDate.getFullYear() + 1,
+ ...events.map(event => new Date(event.date).getFullYear())
+ ])
+ ).sort((a, b) => a - b);
+
+ // Filter events for selected pet, month, and year
+ const filteredEvents = events.filter(event => {
+ if (event.petId !== selectedPetId) return false;
+ const eventDate = new Date(event.date);
+ return eventDate.getMonth() === selectedMonth && eventDate.getFullYear() === selectedYear;
+ });
+
+ // Get days in the current month
+ const getDaysInMonth = (month: number, year: number) => {
+ return new Date(year, month + 1, 0).getDate();
+ };
+
+ // Get the first day of the month (0 = Sunday, 1 = Monday, etc.)
+ const getFirstDayOfMonth = (month: number, year: number) => {
+ const firstDay = new Date(year, month, 1).getDay();
+ // Convert to Monday = 0, Sunday = 6
+ return firstDay === 0 ? 6 : firstDay - 1;
+ };
+
+ // Navigate months
+ const goToPreviousMonth = () => {
+ if (selectedMonth === 0) {
+ setSelectedMonth(11);
+ setSelectedYear(selectedYear - 1);
+ } else {
+ setSelectedMonth(selectedMonth - 1);
+ }
+ };
+
+ const goToNextMonth = () => {
+ if (selectedMonth === 11) {
+ setSelectedMonth(0);
+ setSelectedYear(selectedYear + 1);
+ } else {
+ setSelectedMonth(selectedMonth + 1);
+ }
+ };
+
+ // Check if a day has events
+ const getEventsForDay = (day: number) => {
+ return filteredEvents.filter(event => {
+ const eventDate = new Date(event.date);
+ return eventDate.getDate() === day;
+ });
+ };
+
+ // Check if day is today
+ const isToday = (day: number) => {
+ return day === currentDate.getDate() &&
+ selectedMonth === currentDate.getMonth() &&
+ selectedYear === currentDate.getFullYear();
+ };
+
+ // Modal handlers
+ const handleOpenModal = () => {
+ setEventTitle('');
+ setEventDescription('');
+ setEventDate(new Date());
+ setSelectedTypeId(visitTypes.length > 0 ? visitTypes[0].id : null);
+ setModalVisible(true);
+ };
+
+ const handleCloseModal = () => {
+ setModalVisible(false);
+ };
+
+ const handleSaveEvent = async () => {
+ if (!eventTitle.trim()) {
+ alert('Anna tapahtumalle otsikko');
+ return;
+ }
+
+ if (!selectedPetId) {
+ alert('Valitse lemmikki ensin');
+ return;
+ }
+
+ if (!selectedTypeId) {
+ alert('Valitse tapahtuman tyyppi');
+ return;
+ }
+
+ try {
+ setSaving(true);
+
+ const eventData = {
+ pet_id: selectedPetId,
+ visit_type_id: selectedTypeId,
+ title: eventTitle.trim(),
+ description: eventDescription.trim(),
+ date: eventDate.toISOString().split('T')[0],
+ completed: false,
+ };
+
+ // Try to save to API, fall back to local state if API is not available
+ try {
+ const newEvent = await calendarService.createEvent(eventData);
+ if (newEvent) {
+ // Refresh events from API
+ const refreshedEvents = await calendarService.getAllEvents();
+ setEvents(refreshedEvents);
+ }
+ } catch (apiError) {
+ console.log('API not available, saving to local state:', apiError);
+ // For now, add to local state with a temporary ID
+ const localEvent: CalendarEvent = {
+ id: Date.now(),
+ petId: selectedPetId,
+ title: eventTitle.trim(),
+ description: eventDescription.trim(),
+ date: eventDate.toISOString().split('T')[0],
+ eventType: 'other', // Default for local storage
+ completed: false,
+ };
+ setEvents([...events, localEvent]);
+ }
+
+ handleCloseModal();
+ } catch (err: any) {
+ console.error('Failed to save event:', err);
+ alert('Tapahtuman tallennus epäonnistui');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleDateChange = (_event: any, selectedDate?: Date) => {
+ setShowDatePicker(false);
+ if (selectedDate) {
+ setEventDate(selectedDate);
+ }
+ };
+
+ const getEventTypeLabel = (type: CalendarEvent['eventType']) => {
+ const labels = {
+ vaccination: 'Rokotus',
+ veterinary: 'Eläinlääkäri',
+ medication: 'Lääkitys',
+ grooming: 'Trimmaus',
+ other: 'Muu'
+ };
+ return labels[type];
+ };
+
+ // Render calendar grid
+ const renderCalendar = () => {
+ const daysInMonth = getDaysInMonth(selectedMonth, selectedYear);
+ const firstDay = getFirstDayOfMonth(selectedMonth, selectedYear);
+ const days: (number | null)[] = [];
+
+ // Add empty cells for days before the first day of month
+ for (let i = 0; i < firstDay; i++) {
+ days.push(null);
+ }
+
+ // Add actual days
+ for (let i = 1; i <= daysInMonth; i++) {
+ days.push(i);
+ }
+
+ return (
+
+ {/* Day headers */}
+
+ {DAYS_OF_WEEK.map((day) => (
+
+ {day}
+
+ ))}
+
+
+ {/* Calendar days */}
+
+ {days.map((day, index) => {
+ if (day === null) {
+ return ;
+ }
+
+ const dayEvents = getEventsForDay(day);
+ const today = isToday(day);
+
+ return (
+ {
+ // TODO: Open day detail view or add event for that day
+ }}
+ >
+
+
+ {day}
+
+ {dayEvents.length > 0 && (
+
+ {dayEvents.slice(0, 3).map((event, idx) => (
+
+ ))}
+ {dayEvents.length > 3 && (
+ +{dayEvents.length - 3}
+ )}
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+ };
+
+ // Render events list for the selected month
+ const renderEventsList = () => {
+ if (filteredEvents.length === 0) {
+ return (
+
+
+
+ Ei tapahtumia tässä kuussa
+
+
+ );
+ }
+
+ // Sort events by date
+ const sortedEvents = [...filteredEvents].sort((a, b) =>
+ new Date(a.date).getTime() - new Date(b.date).getTime()
+ );
+
+ return (
+
+
+ Tapahtumat
+
+ {sortedEvents.map(event => (
+
+
+
+
+
+ {event.title}
+
+
+ {new Date(event.date).getDate()}. {MONTHS[new Date(event.date).getMonth()]}
+
+
+
+ {getEventTypeLabel(event.eventType)}
+
+
+ {event.description && (
+
+ {event.description}
+
+ )}
+
+ ))}
+
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+ Ladataan lemmikkejä...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+ Virhe
+
+
+ {error}
+
+
+
+ );
+ }
+
+ if (pets.length === 0) {
+ return (
+
+
+
+
+ Ei lemmikkejä
+
+
+ Lisää ensin lemmikki
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Pet Tabs */}
+
+ {pets.map((pet) => (
+ setSelectedPetId(pet.id)}
+ style={[
+ styles.tab,
+ selectedPetId === pet.id && styles.selectedTab
+ ]}
+ textStyle={selectedPetId === pet.id ? styles.selectedTabText : styles.unselectedTabText}
+ icon={() => (
+
+ )}
+ >
+ {pet.name}
+
+ ))}
+
+
+
+ {/* Month Navigation */}
+
+
+
+
+
+
+
+ {MONTHS[selectedMonth]}
+
+
+ {selectedYear}
+
+
+
+
+
+
+
+
+ {/* Year Selection */}
+ {availableYears.length > 1 && (
+
+
+ {availableYears.map((year) => (
+ setSelectedYear(year)}
+ showSelectedCheck={false}
+ style={[
+ styles.yearTab,
+ selectedYear === year && styles.selectedYearTab
+ ]}
+ textStyle={selectedYear === year ? styles.selectedYearTabText : styles.unselectedYearTabText}
+ >
+ {year}
+
+ ))}
+
+
+ )}
+
+ {/* Calendar Grid */}
+ {renderCalendar()}
+
+ {/* Events List */}
+ {renderEventsList()}
+
+
+ {/* Add Event FAB */}
+
+
+ {/* Add Event Modal */}
+
+
+
+
+ Lisää tapahtuma
+
+
+
+
+
+
+ setShowDatePicker(true)}>
+ }
+ placeholder="PP-KK-VVVV"
+ placeholderTextColor="rgba(0, 0, 0, 0.3)"
+ textColor={COLORS.onSurface}
+ theme={{ colors: { onSurfaceVariant: 'rgba(0, 0, 0, 0.4)' } }}
+ />
+
+
+ {showDatePicker && (
+
+ )}
+
+
+
+ Tapahtuman tyyppi
+
+
+ {visitTypes.length > 0 ? (
+ visitTypes.map((type) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/mobile/src/screens/HomeScreen.tsx b/mobile/src/screens/HomeScreen.tsx
index d5afb14..44134cd 100644
--- a/mobile/src/screens/HomeScreen.tsx
+++ b/mobile/src/screens/HomeScreen.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useRef } from 'react';
import { View, ScrollView, Animated } from 'react-native';
-import { Text, Card, IconButton } from 'react-native-paper';
+import { Text, Card } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import { useAuth } from '../contexts/AuthContext';
@@ -72,7 +72,7 @@ export default function HomeScreen() {
- console.log('Kalenteri')}>
+ navigation.navigate('Calendar' as never)}>
Kalenteri
diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx
index 30e40f5..0608ef3 100644
--- a/mobile/src/screens/MedicationsScreen.tsx
+++ b/mobile/src/screens/MedicationsScreen.tsx
@@ -415,7 +415,7 @@ export default function MedicationsScreen() {
setShowMedicationDatePicker(true)}>
setShowExpireDatePicker(true)}>
setShowVaccinationDatePicker(true)}>
setShowExpireDatePicker(true)}>
(false);
const [saving, setSaving] = useState(false);
const [showDatePicker, setShowDatePicker] = useState(false);
- const [keyboardHeight, setKeyboardHeight] = useState(0);
const [editingVisitId, setEditingVisitId] = useState(null);
const [isEditMode, setIsEditMode] = useState(false);
const [deleteDialogVisible, setDeleteDialogVisible] = useState(false);
@@ -44,27 +43,6 @@ export default function VisitsScreen() {
const scrollViewRef = useRef(null);
const costsInputRef = useRef(null);
const notesInputRef = useRef(null);
-
- // Handle keyboard events
- useEffect(() => {
- const keyboardDidShowListener = Keyboard.addListener(
- 'keyboardDidShow',
- (e) => {
- setKeyboardHeight(e.endCoordinates.height);
- }
- );
- const keyboardDidHideListener = Keyboard.addListener(
- 'keyboardDidHide',
- () => {
- setKeyboardHeight(0);
- }
- );
-
- return () => {
- keyboardDidShowListener.remove();
- keyboardDidHideListener.remove();
- };
- }, []);
// Form state
const [visitDate, setVisitDate] = useState(new Date().toISOString().split('T')[0]);
@@ -468,7 +446,7 @@ export default function VisitsScreen() {
setShowDatePicker(true)}>
- Käynnin tyyppi *
+ Käynnin tyyppi
setShowDatePicker(true)}>
{
+ try {
+ const response = await apiClient.get('/api/calendar-events');
+
+ if (response.data.success && response.data.data) {
+ return response.data.data.map((event: any) => ({
+ id: event.id,
+ petId: event.pet_id,
+ title: event.title,
+ description: event.description,
+ date: event.date,
+ eventType: event.event_type,
+ completed: event.completed || false,
+ notificationEnabled: event.notification_enabled,
+ notificationTime: event.notification_time,
+ }));
+ }
+
+ return [];
+ } catch (error: any) {
+ console.error('Failed to fetch calendar events:', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get events for a specific pet
+ */
+ async getEventsByPetId(petId: number): Promise {
+ try {
+ const response = await apiClient.get(`/api/calendar-events/pet/${petId}`);
+
+ if (response.data.success && response.data.data) {
+ return response.data.data.map((event: any) => ({
+ id: event.id,
+ petId: event.pet_id,
+ title: event.title,
+ description: event.description,
+ date: event.date,
+ eventType: event.event_type,
+ completed: event.completed || false,
+ notificationEnabled: event.notification_enabled,
+ notificationTime: event.notification_time,
+ }));
+ }
+
+ return [];
+ } catch (error: any) {
+ console.error(`Failed to fetch events for pet ${petId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Get a single event by ID
+ */
+ async getEventById(eventId: number): Promise {
+ try {
+ const response = await apiClient.get(`/api/calendar-events/${eventId}`);
+
+ if (response.data.success && response.data.data) {
+ const event = response.data.data;
+ return {
+ id: event.id,
+ petId: event.pet_id,
+ title: event.title,
+ description: event.description,
+ date: event.date,
+ eventType: event.event_type,
+ completed: event.completed || false,
+ notificationEnabled: event.notification_enabled,
+ notificationTime: event.notification_time,
+ };
+ }
+
+ return null;
+ } catch (error: any) {
+ console.error(`Failed to fetch event ${eventId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Create a new calendar event
+ */
+ async createEvent(eventData: CreateCalendarEventData): Promise {
+ try {
+ const response = await apiClient.post('/api/calendar-events', eventData);
+
+ if (response.data.success && response.data.data) {
+ const event = response.data.data;
+ return {
+ id: event.id,
+ petId: event.pet_id,
+ title: event.title,
+ description: event.description,
+ date: event.date,
+ eventType: event.event_type,
+ completed: event.completed || false,
+ notificationEnabled: event.notification_enabled,
+ notificationTime: event.notification_time,
+ };
+ }
+
+ return null;
+ } catch (error: any) {
+ console.error('Failed to create event:', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Update an existing calendar event
+ */
+ async updateEvent(eventId: number, eventData: UpdateCalendarEventData): Promise {
+ try {
+ const response = await apiClient.put(`/api/calendar-events/${eventId}`, eventData);
+
+ if (response.data.success && response.data.data) {
+ const event = response.data.data;
+ return {
+ id: event.id,
+ petId: event.pet_id,
+ title: event.title,
+ description: event.description,
+ date: event.date,
+ eventType: event.event_type,
+ completed: event.completed || false,
+ notificationEnabled: event.notification_enabled,
+ notificationTime: event.notification_time,
+ };
+ }
+
+ return null;
+ } catch (error: any) {
+ console.error(`Failed to update event ${eventId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Delete a calendar event
+ */
+ async deleteEvent(eventId: number): Promise {
+ try {
+ const response = await apiClient.delete(`/api/calendar-events/${eventId}`);
+ return response.data.success;
+ } catch (error: any) {
+ console.error(`Failed to delete event ${eventId}:`, error);
+ throw error;
+ }
+ },
+
+ /**
+ * Mark an event as completed
+ */
+ async markEventCompleted(eventId: number): Promise {
+ return this.updateEvent(eventId, { completed: true });
+ },
+
+ /**
+ * Mark an event as incomplete
+ */
+ async markEventIncomplete(eventId: number): Promise {
+ return this.updateEvent(eventId, { completed: false });
+ },
+
+ /**
+ * Get upcoming events (events in the future)
+ */
+ async getUpcomingEvents(petId?: number): Promise {
+ try {
+ const events = petId
+ ? await this.getEventsByPetId(petId)
+ : await this.getAllEvents();
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ return events
+ .filter(event => new Date(event.date) >= today && !event.completed)
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
+ } catch (error: any) {
+ console.error('Failed to fetch upcoming events:', error);
+ throw error;
+ }
+ },
+};
+
+export default calendarService;
diff --git a/mobile/src/styles/screenStyles.ts b/mobile/src/styles/screenStyles.ts
index 39cca1b..8f3c75a 100644
--- a/mobile/src/styles/screenStyles.ts
+++ b/mobile/src/styles/screenStyles.ts
@@ -234,7 +234,7 @@ export const profileStyles = StyleSheet.create({
},
logoutButton: {
- borderRadius: COMMON_STYLES.button.borderRadius,
+ marginBottom: 0,
},
});
@@ -1211,4 +1211,360 @@ export const weightsStyles = StyleSheet.create({
modalButton: {
flex: 1,
},
+});
+
+// ============================================
+// CALENDAR SCREEN STYLES
+// ============================================
+export const calendarStyles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.background,
+ },
+
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: SPACING.xl * 2,
+ },
+
+ emptyTitle: {
+ marginTop: SPACING.md,
+ color: COLORS.onSurfaceVariant,
+ },
+
+ emptyText: {
+ marginTop: SPACING.xs,
+ color: COLORS.onSurfaceVariant,
+ textAlign: 'center',
+ },
+
+ fab: {
+ position: 'absolute',
+ margin: SPACING.lg,
+ right: 0,
+ bottom: 0,
+ backgroundColor: COLORS.primaryContainer,
+ },
+
+ tabsContainer: {
+ maxHeight: 70,
+ backgroundColor: COLORS.surface,
+ borderBottomWidth: 1,
+ borderBottomColor: COLORS.surfaceVariant,
+ },
+
+ tabsContent: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.md,
+ gap: SPACING.sm,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexGrow: 1,
+ },
+
+ tab: {
+ marginHorizontal: SPACING.xs,
+ paddingHorizontal: SPACING.md,
+ height: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+
+ selectedTab: {
+ backgroundColor: COLORS.primary,
+ elevation: 3,
+ },
+
+ selectedTabText: {
+ color: '#FFFFFF',
+ fontWeight: '700',
+ fontSize: 15,
+ lineHeight: 20,
+ },
+
+ unselectedTabText: {
+ fontSize: 15,
+ lineHeight: 20,
+ },
+
+ content: {
+ flex: 1,
+ },
+
+ scrollContent: {
+ padding: SPACING.md,
+ paddingBottom: 100,
+ },
+
+ // Month Navigation
+ monthNavigation: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: SPACING.md,
+ paddingHorizontal: SPACING.sm,
+ },
+
+ navButton: {
+ padding: SPACING.xs,
+ borderRadius: 8,
+ },
+
+ monthYearDisplay: {
+ alignItems: 'center',
+ },
+
+ monthText: {
+ fontWeight: '600',
+ color: COLORS.onSurface,
+ },
+
+ yearText: {
+ color: COLORS.onSurfaceVariant,
+ marginTop: 2,
+ },
+
+ // Year Selection
+ yearTabsContainer: {
+ marginBottom: SPACING.md,
+ },
+
+ yearTabsContent: {
+ paddingHorizontal: SPACING.xs,
+ gap: SPACING.xs,
+ },
+
+ yearTab: {
+ backgroundColor: COLORS.surfaceVariant,
+ },
+
+ selectedYearTab: {
+ backgroundColor: COLORS.primary,
+ },
+
+ selectedYearTabText: {
+ color: '#FFFFFF',
+ fontWeight: '600',
+ },
+
+ unselectedYearTabText: {
+ color: COLORS.onSurfaceVariant,
+ },
+
+ // Calendar Grid
+ calendarGrid: {
+ backgroundColor: COLORS.surface,
+ borderRadius: 12,
+ padding: SPACING.sm,
+ marginBottom: SPACING.lg,
+ elevation: 2,
+ },
+
+ weekRow: {
+ flexDirection: 'row',
+ marginBottom: SPACING.xs,
+ },
+
+ dayHeader: {
+ flex: 1,
+ alignItems: 'center',
+ paddingVertical: SPACING.xs,
+ },
+
+ dayHeaderText: {
+ color: COLORS.onSurfaceVariant,
+ fontWeight: '600',
+ },
+
+ daysContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+
+ dayCell: {
+ width: `${100 / 7}%`,
+ aspectRatio: 1,
+ padding: 4,
+ },
+
+ todayCell: {
+ backgroundColor: COLORS.primaryContainer + '30',
+ borderRadius: 8,
+ },
+
+ dayCellContent: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ paddingTop: 4,
+ },
+
+ dayNumber: {
+ color: COLORS.onSurface,
+ fontWeight: '500',
+ },
+
+ todayNumber: {
+ color: COLORS.primary,
+ fontWeight: '700',
+ },
+
+ eventIndicators: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ marginTop: 4,
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 2,
+ },
+
+ eventDot: {
+ width: 6,
+ height: 6,
+ borderRadius: 3,
+ },
+
+ moreEventsText: {
+ fontSize: 9,
+ color: COLORS.onSurfaceVariant,
+ marginLeft: 2,
+ },
+
+ // Events List
+ eventsList: {
+ marginTop: SPACING.md,
+ },
+
+ eventsListTitle: {
+ marginBottom: SPACING.md,
+ fontWeight: '600',
+ color: COLORS.onSurface,
+ },
+
+ emptyEventsContainer: {
+ alignItems: 'center',
+ paddingVertical: SPACING.xl,
+ },
+
+ emptyEventsText: {
+ marginTop: SPACING.sm,
+ color: COLORS.onSurfaceVariant,
+ },
+
+ eventCard: {
+ backgroundColor: COLORS.surface,
+ borderRadius: 12,
+ padding: SPACING.md,
+ marginBottom: SPACING.sm,
+ elevation: 1,
+ },
+
+ eventCardHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.sm,
+ },
+
+ eventCardContent: {
+ flex: 1,
+ },
+
+ eventTitle: {
+ fontWeight: '600',
+ color: COLORS.onSurface,
+ },
+
+ eventDate: {
+ color: COLORS.onSurfaceVariant,
+ marginTop: 2,
+ },
+
+ eventTypeChip: {
+ height: 28,
+ },
+
+ eventDescription: {
+ marginTop: SPACING.sm,
+ color: COLORS.onSurfaceVariant,
+ lineHeight: 20,
+ },
+
+ // Modal
+ modal: {
+ backgroundColor: COLORS.surface,
+ margin: SPACING.lg,
+ padding: SPACING.lg,
+ borderRadius: 16,
+ maxHeight: '80%',
+ },
+
+ modalTitle: {
+ marginBottom: SPACING.lg,
+ fontWeight: '600',
+ color: COLORS.onSurface,
+ },
+
+ input: {
+ marginBottom: SPACING.md,
+ },
+
+ datePickerButton: {
+ marginBottom: SPACING.md,
+ padding: SPACING.md,
+ backgroundColor: COLORS.surfaceVariant,
+ borderRadius: 8,
+ },
+
+ datePickerLabel: {
+ color: COLORS.onSurfaceVariant,
+ marginBottom: SPACING.xs,
+ },
+
+ datePickerValue: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.sm,
+ },
+
+ datePickerText: {
+ color: COLORS.onSurface,
+ fontWeight: '500',
+ },
+
+ typePickerButton: {
+ marginBottom: SPACING.md,
+ padding: SPACING.md,
+ backgroundColor: COLORS.surfaceVariant,
+ borderRadius: 8,
+ },
+
+ typePickerLabel: {
+ color: COLORS.onSurfaceVariant,
+ marginBottom: SPACING.xs,
+ },
+
+ typePickerValue: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.sm,
+ },
+
+ typePickerText: {
+ flex: 1,
+ color: COLORS.onSurface,
+ fontWeight: '500',
+ },
+
+ modalActions: {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ gap: SPACING.sm,
+ marginTop: SPACING.lg,
+ },
+
+ modalButton: {
+ minWidth: 100,
+ },
});
\ No newline at end of file
diff --git a/mobile/src/types/index.ts b/mobile/src/types/index.ts
index 56c3539..2e9215a 100644
--- a/mobile/src/types/index.ts
+++ b/mobile/src/types/index.ts
@@ -86,3 +86,16 @@ export interface WalkSettings {
autoStartOnMovement: boolean;
trackSteps: boolean;
}
+
+// Calendar Event types
+export interface CalendarEvent {
+ id: number;
+ petId: number;
+ title: string;
+ description?: string;
+ date: string;
+ eventType: 'vaccination' | 'veterinary' | 'medication' | 'grooming' | 'other';
+ completed: boolean;
+ notificationEnabled?: boolean;
+ notificationTime?: string;
+}