From 1e95303facd6677b045badcd3960aeb52213bc6f Mon Sep 17 00:00:00 2001 From: Samu Date: Wed, 4 Feb 2026 21:05:27 +0200 Subject: [PATCH 1/2] base for calendar screen --- mobile/src/navigation/Navigation.tsx | 9 ++ mobile/src/screens/CalendarScreen.tsx | 147 ++++++++++++++++++++++++++ mobile/src/screens/HomeScreen.tsx | 2 +- mobile/src/styles/screenStyles.ts | 85 +++++++++++++++ 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 mobile/src/screens/CalendarScreen.tsx 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..e1d4c49 --- /dev/null +++ b/mobile/src/screens/CalendarScreen.tsx @@ -0,0 +1,147 @@ +import React, { useState, useEffect } from 'react'; +import { View, ScrollView } from 'react-native'; +import { Text, Chip, ActivityIndicator, FAB } from 'react-native-paper'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { calendarStyles as styles } from '../styles/screenStyles'; +import { COLORS, SPACING } from '../styles/theme'; +import apiClient from '../services/api'; +import { Pet } from '../types'; + +export default function CalendarScreen() { + const [pets, setPets] = useState([]); + const [selectedPetId, setSelectedPetId] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch pets from the API + useEffect(() => { + const fetchPets = 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); + } + } + } catch (err: any) { + console.error('Failed to fetch pets:', err); + setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); + } finally { + setLoading(false); + } + }; + + fetchPets(); + }, []); + + const renderEmptyState = () => ( + + + + Ei tapahtumia + + + Kalenteritoiminnot tulossa pian + + + ); + + if (loading) { + return ( + + + + + Ladataan lemmikkejä... + + + + ); + } + + if (error) { + return ( + + + + + Virhe + + + {error} + + + + ); + } + + if (pets.length === 0) { + return ( + + + + + Ei lemmikkejä + + + Lisää ensin lemmikki + + + + ); + } + + return ( + + + {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} + + ))} + + + + {renderEmptyState()} + + console.log('Lisää tapahtuma')} + label="Lisää Tapahtuma" + /> + + ); +} diff --git a/mobile/src/screens/HomeScreen.tsx b/mobile/src/screens/HomeScreen.tsx index d5afb14..c5c756b 100644 --- a/mobile/src/screens/HomeScreen.tsx +++ b/mobile/src/screens/HomeScreen.tsx @@ -72,7 +72,7 @@ export default function HomeScreen() { - console.log('Kalenteri')}> + navigation.navigate('Calendar' as never)}> Kalenteri diff --git a/mobile/src/styles/screenStyles.ts b/mobile/src/styles/screenStyles.ts index 39cca1b..311e18d 100644 --- a/mobile/src/styles/screenStyles.ts +++ b/mobile/src/styles/screenStyles.ts @@ -1211,4 +1211,89 @@ 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, + }, }); \ No newline at end of file From 9b4b5f80b469b94624ceb0d904caada0002c5659 Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 5 Feb 2026 10:47:19 +0200 Subject: [PATCH 2/2] implemented calendar view and modal to add a new calendar event, date formatting in all modals to 'toLocaleDateString('fi-FI') --- mobile/src/screens/CalendarScreen.tsx | 556 +++++++++++++++++- mobile/src/screens/HomeScreen.tsx | 2 +- mobile/src/screens/MedicationsScreen.tsx | 4 +- mobile/src/screens/VaccinationsScreen.tsx | 4 +- mobile/src/screens/VisitsScreen.tsx | 28 +- mobile/src/screens/WeightManagementScreen.tsx | 2 +- mobile/src/services/calendarService.ts | 232 ++++++++ mobile/src/styles/screenStyles.ts | 273 ++++++++- mobile/src/types/index.ts | 13 + 9 files changed, 1062 insertions(+), 52 deletions(-) create mode 100644 mobile/src/services/calendarService.ts diff --git a/mobile/src/screens/CalendarScreen.tsx b/mobile/src/screens/CalendarScreen.tsx index e1d4c49..49d102a 100644 --- a/mobile/src/screens/CalendarScreen.tsx +++ b/mobile/src/screens/CalendarScreen.tsx @@ -1,21 +1,65 @@ import React, { useState, useEffect } from 'react'; -import { View, ScrollView } from 'react-native'; -import { Text, Chip, ActivityIndicator, FAB } from 'react-native-paper'; +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 { Pet } from '../types'; +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 from the API + // Fetch pets and events from the API useEffect(() => { - const fetchPets = async () => { + const fetchData = async () => { try { setLoading(true); setError(null); @@ -31,28 +75,331 @@ export default function CalendarScreen() { 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 pets:', err); + console.error('Failed to fetch data:', err); setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { setLoading(false); } }; - fetchPets(); + fetchData(); }, []); - const renderEmptyState = () => ( - - - - Ei tapahtumia - - - Kalenteritoiminnot tulossa pian - - - ); + // 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 ( @@ -101,6 +448,7 @@ export default function CalendarScreen() { return ( + {/* Pet Tabs */} - {renderEmptyState()} + {/* 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 */} console.log('Lisää tapahtuma')} + onPress={handleOpenModal} label="Lisää Tapahtuma" /> + + {/* 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 c5c756b..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'; 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 311e18d..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, }, }); @@ -1295,5 +1295,276 @@ export const calendarStyles = StyleSheet.create({ 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; +}