diff --git a/mobile/src/navigation/Navigation.tsx b/mobile/src/navigation/Navigation.tsx index 5a616f9..d19b375 100644 --- a/mobile/src/navigation/Navigation.tsx +++ b/mobile/src/navigation/Navigation.tsx @@ -21,6 +21,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'; // Added this root stack params list while trying to fix pet navigation issues export type RootStackParamList = { @@ -246,6 +247,30 @@ 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/HealthScreen.tsx b/mobile/src/screens/HealthScreen.tsx index 523a468..5c0147b 100644 --- a/mobile/src/screens/HealthScreen.tsx +++ b/mobile/src/screens/HealthScreen.tsx @@ -37,41 +37,29 @@ export default function HealthScreen() { navigation.navigate('Visits' as never)}> - - Käynnit - - Eläinlääkäri- ja klinikkakäynnit - + + Käynnit navigation.navigate('Medications' as never)}> - - Lääkitykset - - Lääkkeet ja hoito-ohjelmat - + + Lääkitykset navigation.navigate('Vaccinations' as never)}> - - Rokotukset - - Rokotushistoria ja muistutukset - + + Rokotukset navigation.navigate('WeightManagement' as never)}> - - Painonhallinta - - Seuraa painoa ja kasvua - + + Paino diff --git a/mobile/src/screens/HomeScreen.tsx b/mobile/src/screens/HomeScreen.tsx index 7a12aa7..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'; @@ -52,7 +52,7 @@ export default function HomeScreen() { - console.log('Lemmikki')}> + navigation.navigate('Pets' as never)}> Lemmikki @@ -72,7 +72,7 @@ export default function HomeScreen() { - console.log('Kalenteri')}> + navigation.navigate('Calendar' as never)}> Kalenteri @@ -112,7 +112,7 @@ export default function HomeScreen() { - console.log('Asetukset')}> + navigation.navigate('Settings' as never)}> Asetukset diff --git a/mobile/src/screens/MapScreen.tsx b/mobile/src/screens/MapScreen.tsx index 4819493..9f9d0f0 100644 --- a/mobile/src/screens/MapScreen.tsx +++ b/mobile/src/screens/MapScreen.tsx @@ -33,8 +33,8 @@ export default function MapScreen() { // TODO: Hae lemmikit backendistä // Väliaikaisesti kovakoodatut lemmikit setPets([ - { id: '1', name: 'Macho', breed: 'Akita', age: 12, weight: 44, dateOfBirth: '2013-10-01' }, - { id: '2', name: 'Mirri', breed: 'Sekarotuinen', age: 2, weight: 15, dateOfBirth: '2022-06-15' }, + { id: 1, name: 'Macho', breed: 'Akita', age: 12, weight: 44, dateOfBirth: '2013-10-01' }, + { id: 2, name: 'Mirri', breed: 'Sekarotuinen', age: 2, weight: 15, dateOfBirth: '2022-06-15' }, ]); // Hae käyttäjän sijainti heti diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index e88d657..0608ef3 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -1,12 +1,15 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, StyleSheet, TouchableOpacity } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper'; +import { View, ScrollView, TouchableOpacity } from 'react-native'; +import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button, Dialog } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { medicationsStyles as styles } from '../styles/screenStyles'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; +import { medicationsService } from '../services/medicationsService'; import { Pet } from '../types'; import DateTimePicker from '@react-native-community/datetimepicker'; + interface Medication { id: number; pet_id: number; @@ -16,7 +19,7 @@ interface Medication { costs?: string; notes?: string; } - + export default function MedicationsScreen() { const [pets, setPets] = useState([]); const [medications, setMedications] = useState([]); @@ -27,6 +30,10 @@ export default function MedicationsScreen() { const [saving, setSaving] = useState(false); const [showMedicationDatePicker, setShowMedicationDatePicker] = useState(false); const [showExpireDatePicker, setShowExpireDatePicker] = useState(false); + const [editingMedicationId, setEditingMedicationId] = useState(null); + const [isEditMode, setIsEditMode] = useState(false); + const [deleteDialogVisible, setDeleteDialogVisible] = useState(false); + const [medicationToDelete, setMedicationToDelete] = useState(null); const scrollViewRef = useRef(null); @@ -44,9 +51,9 @@ export default function MedicationsScreen() { setLoading(true); setError(null); - const [petsResponse, medicationsResponse] = await Promise.all([ + const [petsResponse, fetchedMedications] = await Promise.all([ apiClient.get('/api/pets'), - apiClient.get('/api/medications') + medicationsService.getAllMedications() ]); if (petsResponse.data.success && petsResponse.data.data) { @@ -58,27 +65,7 @@ export default function MedicationsScreen() { } } - if (medicationsResponse.data.success && medicationsResponse.data.data) { - const medicationsData = medicationsResponse.data.data; - - if (Array.isArray(medicationsData) && medicationsData.length > 0 && medicationsData[0].medications) { - const flattenedMedications: Medication[] = []; - medicationsData.forEach((petMedGroup: any) => { - const petId = petMedGroup.pet_id; - if (petMedGroup.medications && Array.isArray(petMedGroup.medications)) { - petMedGroup.medications.forEach((med: any) => { - flattenedMedications.push({ - ...med, - pet_id: petId - }); - }); - } - }); - setMedications(flattenedMedications); - } else { - setMedications(medicationsData); - } - } + setMedications(fetchedMedications); } catch (err: any) { setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { @@ -94,6 +81,8 @@ export default function MedicationsScreen() { .sort((a, b) => new Date(b.medication_date).getTime() - new Date(a.medication_date).getTime()); const handleOpenModal = () => { + setIsEditMode(false); + setEditingMedicationId(null); setMedName(''); setMedicationDate(new Date().toISOString().split('T')[0]); setExpireDate(''); @@ -104,53 +93,62 @@ export default function MedicationsScreen() { const handleCloseModal = () => { setModalVisible(false); + setIsEditMode(false); + setEditingMedicationId(null); }; const handleSaveMedication = async () => { - if (!selectedPetId || !medName) { + if (!medName) { alert('Täytä kaikki pakolliset kentät'); return; } try { setSaving(true); + + if (isEditMode && editingMedicationId) { + + const medicationData = { + med_name: medName, + medication_date: medicationDate, + expire_date: expireDate || undefined, + costs: costs ? parseFloat(costs) : undefined, + notes: notes || undefined + }; + + const updatedMedication = await medicationsService.updateMedication(editingMedicationId, medicationData); + + if (updatedMedication) { + // Refresh medications + const refreshedMedications = await medicationsService.getAllMedications(); + setMedications(refreshedMedications); + + handleCloseModal(); + } + } else { + if (!selectedPetId) { + alert('Valitse lemmikki ennen tallentamista.'); + return; + } - const medicationData = { - pet_id: selectedPetId, - med_name: medName, - medication_date: medicationDate, - expire_date: expireDate || undefined, - costs: costs ? parseFloat(costs) : undefined, - notes: notes || undefined - }; - - const response = await apiClient.post('/api/medications', medicationData); - - if (response.data.success) { - const medicationsResponse = await apiClient.get('/api/medications'); - if (medicationsResponse.data.success && medicationsResponse.data.data) { - const medicationsData = medicationsResponse.data.data; + const medicationData = { + pet_id: selectedPetId, + med_name: medName, + medication_date: medicationDate, + expire_date: expireDate || undefined, + costs: costs ? parseFloat(costs) : undefined, + notes: notes || undefined + }; + + const newMedication = await medicationsService.createMedication(medicationData); + + if (newMedication) { + // Refresh medications + const refreshedMedications = await medicationsService.getAllMedications(); + setMedications(refreshedMedications); - if (Array.isArray(medicationsData) && medicationsData.length > 0 && medicationsData[0].medications) { - const flattenedMedications: Medication[] = []; - medicationsData.forEach((petMedGroup: any) => { - const petId = petMedGroup.pet_id; - if (petMedGroup.medications && Array.isArray(petMedGroup.medications)) { - petMedGroup.medications.forEach((med: any) => { - flattenedMedications.push({ - ...med, - pet_id: petId - }); - }); - } - }); - setMedications(flattenedMedications); - } else { - setMedications(medicationsData); - } + handleCloseModal(); } - - handleCloseModal(); } } catch (err: any) { alert('Lääkityksen tallentaminen epäonnistui. Yritä uudelleen.'); @@ -159,6 +157,47 @@ export default function MedicationsScreen() { } }; + const handleEditMedication = (medication: Medication) => { + + setIsEditMode(true); + setEditingMedicationId(medication.id); + + const dateOnly = medication.medication_date.split('T')[0]; + setMedName(medication.med_name); + setMedicationDate(dateOnly); + const expireDateOnly = medication.expire_date ? medication.expire_date.split('T')[0] : ''; + setExpireDate(expireDateOnly); + setCosts(medication.costs || ''); + setNotes(medication.notes || ''); + setModalVisible(true); + }; + + const handleDeleteMedication = (medication: Medication) => { + setMedicationToDelete(medication); + setDeleteDialogVisible(true); + }; + + const confirmDeleteMedication = async () => { + if (!medicationToDelete) return; + + try { + const success = await medicationsService.deleteMedication(medicationToDelete.id); + + if (success) { + // Refresh medications + const refreshedMedications = await medicationsService.getAllMedications(); + setMedications(refreshedMedications); + setDeleteDialogVisible(false); + setMedicationToDelete(null); + } else { + alert('Lääkityksen poistaminen epäonnistui. Yritä uudelleen.'); + } + } catch (err: any) { + console.error("Failed to delete medication:", err); + alert('Lääkityksen poistaminen epäonnistui. Yritä uudelleen.'); + } + }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -211,17 +250,29 @@ export default function MedicationsScreen() { )} - {medication.notes && ( - <> - + + + + {medication.notes ? ( {medication.notes} - - )} + ) : ( + + )} + + + handleEditMedication(medication)} style={styles.actionButton}> + + + handleDeleteMedication(medication)} style={styles.actionButton}> + + + + ); @@ -346,7 +397,7 @@ export default function MedicationsScreen() { contentContainerStyle={styles.scrollContentContainer} > - Lisää lääkitys + {isEditMode ? 'Muokkaa lääkitystä' : 'Lisää lääkitys'} setShowMedicationDatePicker(true)}> setShowExpireDatePicker(true)}> + setDeleteDialogVisible(false)} + > + Vahvista poisto + + + Haluatko varmasti poistaa tämän lääkityksen? + + + + + + + ); } -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: COLORS.background, - }, - 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, - }, - medicationCard: { - marginBottom: SPACING.md, - elevation: 2, - }, - medicationHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING.sm, - }, - medicationName: { - fontWeight: '700', - color: COLORS.primary, - flex: 1, - }, - dosageContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.xs, - marginBottom: SPACING.sm, - }, - dosage: { - fontWeight: '600', - color: COLORS.onSurface, - }, - divider: { - marginVertical: SPACING.sm, - }, - notesContainer: { - flexDirection: 'row', - gap: SPACING.sm, - marginTop: SPACING.xs, - }, - notesText: { - flex: 1, - color: COLORS.onSurfaceVariant, - fontStyle: 'italic', - }, - 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', - right: SPACING.md, - bottom: SPACING.md, - backgroundColor: COLORS.primary, - }, - modalContainer: { - backgroundColor: COLORS.surface, - margin: SPACING.lg, - padding: SPACING.lg, - borderRadius: 12, - maxHeight: '90%', - }, - scrollContentContainer: { - paddingBottom: 0, - }, - modalTitle: { - marginBottom: SPACING.lg, - fontWeight: 'bold', - color: COLORS.onSurface, - }, - input: { - marginBottom: SPACING.md, - }, - modalButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: SPACING.lg, - gap: SPACING.md, - }, - modalButton: { - flex: 1, - }, -}); diff --git a/mobile/src/screens/ProfileScreen.tsx b/mobile/src/screens/ProfileScreen.tsx index 9d56870..e6f588b 100644 --- a/mobile/src/screens/ProfileScreen.tsx +++ b/mobile/src/screens/ProfileScreen.tsx @@ -630,6 +630,16 @@ export default function ProfileScreen() { > Hoitaja + diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index b6ce6d5..819829c 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -1,9 +1,11 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, StyleSheet, TouchableOpacity } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper'; +import { View, ScrollView, TouchableOpacity } from 'react-native'; +import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button, Dialog } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { vaccinationsStyles as styles } from '../styles/screenStyles'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; +import { vaccinationsService } from '../services/vaccinationsService'; import { Pet } from '../types'; import DateTimePicker from '@react-native-community/datetimepicker'; @@ -27,6 +29,10 @@ export default function VaccinationsScreen() { const [saving, setSaving] = useState(false); const [showVaccinationDatePicker, setShowVaccinationDatePicker] = useState(false); const [showExpireDatePicker, setShowExpireDatePicker] = useState(false); + const [editingVaccinationId, setEditingVaccinationId] = useState(null); + const [isEditMode, setIsEditMode] = useState(false); + const [deleteDialogVisible, setDeleteDialogVisible] = useState(false); + const [vaccinationToDelete, setVaccinationToDelete] = useState(null); const scrollViewRef = useRef(null); @@ -44,9 +50,9 @@ export default function VaccinationsScreen() { setLoading(true); setError(null); - const [petsResponse, vaccinationsResponse] = await Promise.all([ + const [petsResponse, fetchedVaccinations] = await Promise.all([ apiClient.get('/api/pets'), - apiClient.get('/api/vaccinations') + vaccinationsService.getAllVaccinations() ]); if (petsResponse.data.success && petsResponse.data.data) { @@ -58,27 +64,7 @@ export default function VaccinationsScreen() { } } - if (vaccinationsResponse.data.success && vaccinationsResponse.data.data) { - const vaccinationsData = vaccinationsResponse.data.data; - - if (Array.isArray(vaccinationsData) && vaccinationsData.length > 0 && vaccinationsData[0].vaccinations) { - const flattenedVaccinations: Vaccination[] = []; - vaccinationsData.forEach((petVacGroup: any) => { - const petId = petVacGroup.pet_id; - if (petVacGroup.vaccinations && Array.isArray(petVacGroup.vaccinations)) { - petVacGroup.vaccinations.forEach((vac: any) => { - flattenedVaccinations.push({ - ...vac, - pet_id: petId - }); - }); - } - }); - setVaccinations(flattenedVaccinations); - } else { - setVaccinations(vaccinationsData); - } - } + setVaccinations(fetchedVaccinations); } catch (err: any) { setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { @@ -94,6 +80,9 @@ export default function VaccinationsScreen() { .sort((a, b) => new Date(b.vaccination_date).getTime() - new Date(a.vaccination_date).getTime()); const handleOpenModal = () => { + // Reset form for create mode + setIsEditMode(false); + setEditingVaccinationId(null); setVacName(''); setVaccinationDate(new Date().toISOString().split('T')[0]); setExpireDate(''); @@ -104,53 +93,63 @@ export default function VaccinationsScreen() { const handleCloseModal = () => { setModalVisible(false); + setIsEditMode(false); + setEditingVaccinationId(null); }; const handleSaveVaccination = async () => { - if (!selectedPetId || !vacName) { + if (!vacName) { alert('Täytä kaikki pakolliset kentät'); return; } try { setSaving(true); + + if (isEditMode && editingVaccinationId) { - const vaccinationData = { - pet_id: selectedPetId, - vac_name: vacName, - vaccination_date: vaccinationDate, - expire_date: expireDate || undefined, - costs: costs ? parseFloat(costs) : undefined, - notes: notes || undefined - }; - - const response = await apiClient.post('/api/vaccinations', vaccinationData); - - if (response.data.success) { - const vaccinationsResponse = await apiClient.get('/api/vaccinations'); - if (vaccinationsResponse.data.success && vaccinationsResponse.data.data) { - const vaccinationsData = vaccinationsResponse.data.data; + const vaccinationData = { + pet_id: selectedPetId, + vac_name: vacName, + vaccination_date: vaccinationDate, + expire_date: expireDate || undefined, + costs: costs ? parseFloat(costs) : undefined, + notes: notes || undefined + }; + + const updatedVaccination = await vaccinationsService.updateVaccination(editingVaccinationId, vaccinationData); + + if (updatedVaccination) { + // Refresh vaccinations + const refreshedVaccinations = await vaccinationsService.getAllVaccinations(); + setVaccinations(refreshedVaccinations); - if (Array.isArray(vaccinationsData) && vaccinationsData.length > 0 && vaccinationsData[0].vaccinations) { - const flattenedVaccinations: Vaccination[] = []; - vaccinationsData.forEach((petVacGroup: any) => { - const petId = petVacGroup.pet_id; - if (petVacGroup.vaccinations && Array.isArray(petVacGroup.vaccinations)) { - petVacGroup.vaccinations.forEach((vac: any) => { - flattenedVaccinations.push({ - ...vac, - pet_id: petId - }); - }); - } - }); - setVaccinations(flattenedVaccinations); - } else { - setVaccinations(vaccinationsData); - } + handleCloseModal(); + } + } else { + if (!selectedPetId) { + alert('Valitse lemmikki ennen tallentamista.'); + return; + } + + const vaccinationData = { + pet_id: selectedPetId, + vac_name: vacName, + vaccination_date: vaccinationDate, + expire_date: expireDate || undefined, + costs: costs ? parseFloat(costs) : undefined, + notes: notes || undefined + }; + + const newVaccination = await vaccinationsService.createVaccination(vaccinationData); + + if (newVaccination) { + // Refresh vaccinations + const refreshedVaccinations = await vaccinationsService.getAllVaccinations(); + setVaccinations(refreshedVaccinations); + + handleCloseModal(); } - - handleCloseModal(); } } catch (err: any) { alert('Rokotuksen tallentaminen epäonnistui. Yritä uudelleen.'); @@ -159,6 +158,46 @@ export default function VaccinationsScreen() { } }; + const handleEditVaccination = (vaccination: Vaccination) => { + setIsEditMode(true); + setEditingVaccinationId(vaccination.id); + + const dateOnly = vaccination.vaccination_date.split('T')[0]; + setVacName(vaccination.vac_name); + setVaccinationDate(dateOnly); + const expireDateOnly = vaccination.expire_date ? vaccination.expire_date.split('T')[0] : ''; + setExpireDate(expireDateOnly); + setCosts(vaccination.costs ? vaccination.costs.toString() : ''); + setNotes(vaccination.notes || ''); + setModalVisible(true); + }; + + const handleDeleteVaccination = async (vaccination: Vaccination) => { + setVaccinationToDelete(vaccination); + setDeleteDialogVisible(true); + }; + + const confirmDeleteVaccination = async () => { + if (!vaccinationToDelete) return; + + try { + const success = await vaccinationsService.deleteVaccination(vaccinationToDelete.id); + + if (success) { + // Refresh vaccinations + const refreshedVaccinations = await vaccinationsService.getAllVaccinations(); + setVaccinations(refreshedVaccinations); + setDeleteDialogVisible(false); + setVaccinationToDelete(null); + } else { + alert('Rokotuksen poistaminen epäonnistui. Yritä uudelleen.'); + } + } catch (err: any) { + console.error("Failed to delete vaccination:", err); + alert('Rokotuksen poistaminen epäonnistui. Yritä uudelleen.'); + } + }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -211,17 +250,29 @@ export default function VaccinationsScreen() { )} - {vaccination.notes && ( - <> - + + + + {vaccination.notes ? ( {vaccination.notes} - - )} + ) : ( + + )} + + + handleEditVaccination(vaccination)} style={styles.actionButton}> + + + handleDeleteVaccination(vaccination)} style={styles.actionButton}> + + + + ); @@ -346,7 +397,7 @@ export default function VaccinationsScreen() { contentContainerStyle={styles.scrollContentContainer} > - Lisää rokotus + {isEditMode ? 'Muokkaa rokotusta' : 'Lisää rokotus'} setShowVaccinationDatePicker(true)}> setShowExpireDatePicker(true)}> + setDeleteDialogVisible(false)} + > + Vahvista poisto + + Haluatko varmasti poistaa tämän rokotuksen? + + + + + + ); } -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: COLORS.background, - }, - 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, - }, - vaccinationCard: { - marginBottom: SPACING.md, - elevation: 2, - }, - vaccinationHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING.sm, - }, - vaccinationName: { - fontWeight: '700', - color: COLORS.primary, - flex: 1, - }, - dateContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.xs, - marginBottom: SPACING.sm, - }, - dateText: { - fontWeight: '600', - color: COLORS.onSurface, - }, - divider: { - marginVertical: SPACING.sm, - }, - notesContainer: { - flexDirection: 'row', - gap: SPACING.sm, - marginTop: SPACING.xs, - }, - notesText: { - flex: 1, - color: COLORS.onSurfaceVariant, - fontStyle: 'italic', - }, - 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', - right: SPACING.md, - bottom: SPACING.md, - backgroundColor: COLORS.primary, - }, - modalContainer: { - backgroundColor: COLORS.surface, - margin: SPACING.lg, - padding: SPACING.lg, - borderRadius: 12, - maxHeight: '90%', - }, - scrollContentContainer: { - paddingBottom: 0, - }, - modalTitle: { - marginBottom: SPACING.lg, - fontWeight: 'bold', - color: COLORS.onSurface, - }, - input: { - marginBottom: SPACING.md, - }, - modalButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: SPACING.lg, - gap: SPACING.md, - }, - modalButton: { - flex: 1, - }, -}); diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index 145a1d5..d5ba6db 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -1,9 +1,11 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, StyleSheet, TouchableOpacity, Keyboard } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper'; +import { View, ScrollView, TouchableOpacity } from 'react-native'; +import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button, Dialog } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { visitsStyles as styles } from '../styles/screenStyles'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; +import { visitsService } from '../services/visitsService'; import { Pet } from '../types'; import DateTimePicker from '@react-native-community/datetimepicker'; @@ -33,32 +35,14 @@ export default function VisitsScreen() { const [modalVisible, setModalVisible] = useState(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); + const [visitToDelete, setVisitToDelete] = useState(null); 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]); @@ -76,9 +60,9 @@ export default function VisitsScreen() { setError(null); // Fetch pets and visits in parallel - const [petsResponse, visitsResponse] = await Promise.all([ + const [petsResponse, fetchedVisits] = await Promise.all([ apiClient.get('/api/pets'), - apiClient.get('/api/vet-visits') + visitsService.getAllVisits() ]); if (petsResponse.data.success && petsResponse.data.data) { @@ -91,22 +75,7 @@ export default function VisitsScreen() { } } - if (visitsResponse.data.success && visitsResponse.data.data) { - // Flatten the nested structure: each pet has a vet_visits array - const flattenedVisits: Visit[] = []; - visitsResponse.data.data.forEach((petVisitGroup: any) => { - const petId = petVisitGroup.pet_id; - if (petVisitGroup.vet_visits && Array.isArray(petVisitGroup.vet_visits)) { - petVisitGroup.vet_visits.forEach((visit: any) => { - flattenedVisits.push({ - ...visit, - pet_id: petId - }); - }); - } - }); - setVisits(flattenedVisits); - } + setVisits(fetchedVisits); } catch (err: any) { console.error('Failed to fetch data:', err); setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); @@ -123,7 +92,9 @@ export default function VisitsScreen() { .sort((a, b) => new Date(b.visit_date).getTime() - new Date(a.visit_date).getTime()); const handleOpenModal = async () => { - // Reset form + // Reset form for create mode + setIsEditMode(false); + setEditingVisitId(null); setVisitDate(new Date().toISOString().split('T')[0]); setVetName(''); setLocation(''); @@ -135,11 +106,9 @@ export default function VisitsScreen() { // Fetch visit types if not already loaded if (visitTypes.length === 0) { try { - const typesResponse = await apiClient.get('/api/vet-visits/types'); - if (typesResponse.data.success && typesResponse.data.data) { - console.log('Visit types response:', typesResponse.data.data); - setVisitTypes(typesResponse.data.data); - } + const types = await visitsService.getVisitTypes(); + console.log('Visit types response:', types); + setVisitTypes(types); } catch (err: any) { console.error('Failed to fetch visit types:', err); } @@ -148,10 +117,12 @@ export default function VisitsScreen() { const handleCloseModal = () => { setModalVisible(false); + setIsEditMode(false); + setEditingVisitId(null); }; const handleSaveVisit = async () => { - if (!selectedPetId || !vetName || !location || !selectedTypeId) { + if (!vetName || !location || !selectedTypeId) { alert('Täytä kaikki pakolliset kentät'); return; } @@ -159,38 +130,50 @@ export default function VisitsScreen() { try { setSaving(true); - const visitData = { - pet_id: selectedPetId, - visit_date: visitDate, - vet_name: vetName, - location: location, - type_id: selectedTypeId, - notes: notes || undefined, - costs: costs ? parseFloat(costs) : undefined - }; - - const response = await apiClient.post('/api/vet-visits', visitData); - - if (response.data.success) { - // Refresh visits - const visitsResponse = await apiClient.get('/api/vet-visits'); - if (visitsResponse.data.success && visitsResponse.data.data) { - const flattenedVisits: Visit[] = []; - visitsResponse.data.data.forEach((petVisitGroup: any) => { - const petId = petVisitGroup.pet_id; - if (petVisitGroup.vet_visits && Array.isArray(petVisitGroup.vet_visits)) { - petVisitGroup.vet_visits.forEach((visit: any) => { - flattenedVisits.push({ - ...visit, - pet_id: petId - }); - }); - } - }); - setVisits(flattenedVisits); + if (isEditMode && editingVisitId) { + + const visitData = { + visit_date: visitDate, + vet_name: vetName, + location: location, + type_id: selectedTypeId, + notes: notes || undefined, + costs: costs ? parseFloat(costs) : undefined + }; + + const updatedVisit = await visitsService.updateVisit(editingVisitId, visitData); + + if (updatedVisit) { + // Refresh visits + const refreshedVisits = await visitsService.getAllVisits(); + setVisits(refreshedVisits); + handleCloseModal(); + } + } else { + // Create new visit + if (!selectedPetId) { + alert('Valitse lemmikki'); + return; } - handleCloseModal(); + const visitData = { + pet_id: selectedPetId, + visit_date: visitDate, + vet_name: vetName, + location: location, + type_id: selectedTypeId, + notes: notes || undefined, + costs: costs ? parseFloat(costs) : undefined + }; + + const newVisit = await visitsService.createVisit(visitData); + + if (newVisit) { + // Refresh visits + const refreshedVisits = await visitsService.getAllVisits(); + setVisits(refreshedVisits); + handleCloseModal(); + } } } catch (err: any) { console.error('Failed to save visit:', err); @@ -200,6 +183,7 @@ export default function VisitsScreen() { } }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -209,6 +193,67 @@ export default function VisitsScreen() { }); }; + const handleEditVisit = async (visit: Visit) => { + // Set edit mode + setIsEditMode(true); + setEditingVisitId(visit.id); + + // Fetch visit types first if not already loaded + let types = visitTypes; + if (types.length === 0) { + try { + types = await visitsService.getVisitTypes(); + setVisitTypes(types); + } catch (err: any) { + console.error('Failed to fetch visit types:', err); + } + } + + // Pre-fill form with visit data + // Extract date only (remove timestamp if present) + const dateOnly = visit.visit_date.split('T')[0]; + setVisitDate(dateOnly); + setVetName(visit.vet_name); + setLocation(visit.location); + + // Find the type by name (since type_id stores the name, not ID) + const matchingType = types.find(t => t.name.toLowerCase() === visit.type_id.toLowerCase()); + if (matchingType) { + setSelectedTypeId(matchingType.id); + } + + setNotes(visit.notes || ''); + setCosts(visit.costs || ''); + + setModalVisible(true); + }; + + const handleDeleteVisit = (visit: Visit) => { + setVisitToDelete(visit); + setDeleteDialogVisible(true); + }; + + const confirmDeleteVisit = async () => { + if (!visitToDelete) return; + + try { + const success = await visitsService.deleteVisit(visitToDelete.id); + + if (success) { + // Refresh visits + const refreshedVisits = await visitsService.getAllVisits(); + setVisits(refreshedVisits); + setDeleteDialogVisible(false); + setVisitToDelete(null); + } else { + alert('Käynnin poistaminen epäonnistui.'); + } + } catch (err: any) { + console.error('Failed to delete visit:', err); + alert('Käynnin poistaminen epäonnistui. Yritä uudelleen.'); + } + }; + const renderVisitCard = (visit: Visit) => ( @@ -249,17 +294,29 @@ export default function VisitsScreen() { - {visit.notes && ( - <> - + + + + {visit.notes ? ( {visit.notes} - - )} + ) : ( + + )} + + + handleEditVisit(visit)} style={styles.actionButton}> + + + handleDeleteVisit(visit)} style={styles.actionButton}> + + + + ); @@ -383,13 +440,13 @@ export default function VisitsScreen() { contentContainerStyle={styles.scrollContentContainer} > - Lisää käynti + {isEditMode ? 'Muokkaa käyntiä' : 'Lisää käynti'} setShowDatePicker(true)}> - Käynnin tyyppi * + Käynnin tyyppi + + setDeleteDialogVisible(false)} + > + Poista käynti + + + {visitToDelete && `Haluatko varmasti poistaa käynnin ${formatDate(visitToDelete.visit_date)}?`} + + + + + + + ); } -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: COLORS.background, - }, - header: { - paddingHorizontal: SPACING.lg, - paddingTop: SPACING.lg, - paddingBottom: SPACING.md, - }, - title: { - fontWeight: 'bold', - color: COLORS.onBackground, - }, - 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, - }, - visitCard: { - marginBottom: SPACING.md, - elevation: 2, - }, - visitHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING.sm, - }, - dateContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.xs, - }, - dateText: { - fontWeight: '600', - color: COLORS.primary, - }, - divider: { - marginVertical: SPACING.sm, - }, - visitDetail: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.sm, - marginBottom: SPACING.xs, - }, - detailText: { - flex: 1, - color: COLORS.onSurface, - }, - notesContainer: { - flexDirection: 'row', - gap: SPACING.sm, - marginTop: SPACING.xs, - }, - notesText: { - flex: 1, - color: COLORS.onSurfaceVariant, - fontStyle: 'italic', - }, - 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', - right: SPACING.md, - bottom: SPACING.md, - backgroundColor: COLORS.primary, - }, - modalContainer: { - backgroundColor: COLORS.surface, - margin: SPACING.lg, - padding: SPACING.lg, - borderRadius: 12, - maxHeight: '90%', - }, - keyboardAvoid: { - width: '100%', - }, - scrollContentContainer: { - paddingBottom: 0, - }, - modalTitle: { - marginBottom: SPACING.lg, - fontWeight: 'bold', - color: COLORS.onSurface, - }, - input: { - marginBottom: SPACING.md, - }, - modalButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: SPACING.lg, - gap: SPACING.md, - }, - modalButton: { - flex: 1, - }, -}); diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx index 6b46af9..f956f8b 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -1,11 +1,13 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, StyleSheet, Dimensions, TouchableOpacity } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, Button, TextInput } from 'react-native-paper'; +import { View, ScrollView, Dimensions, TouchableOpacity } from 'react-native'; +import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, Button, TextInput, Dialog } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import Svg, { Line, Circle } from 'react-native-svg'; import DateTimePicker from '@react-native-community/datetimepicker'; +import { weightsStyles as styles } from '../styles/screenStyles'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; +import { weightsService } from '../services/weightsService'; import { Pet } from '../types'; interface WeightRecord { @@ -28,6 +30,10 @@ export default function WeightManagementScreen() { const [modalVisible, setModalVisible] = useState(false); const [saving, setSaving] = useState(false); const [showDatePicker, setShowDatePicker] = useState(false); + const [editingWeightId, setEditingWeightId] = useState(null); + const [isEditMode, setIsEditMode] = useState(false); + const [deleteDialogVisible, setDeleteDialogVisible] = useState(false); + const [weightToDelete, setWeightToDelete] = useState(null); const scrollViewRef = useRef(null); @@ -43,9 +49,9 @@ export default function WeightManagementScreen() { setError(null); // Fetch both pets and weights in parallel - const [petsResponse, weightsResponse] = await Promise.all([ + const [petsResponse, fetchedWeights] = await Promise.all([ apiClient.get('/api/pets'), - apiClient.get('/api/weights') + weightsService.getAllWeights() ]); if (petsResponse.data.success && petsResponse.data.data) { @@ -58,30 +64,7 @@ export default function WeightManagementScreen() { } } - if (weightsResponse.data.success && weightsResponse.data.data) { - const weightsData = weightsResponse.data.data; - - // If nested structure (array of pet weight groups) - if (Array.isArray(weightsData) && weightsData.length > 0 && weightsData[0].weights) { - const flattenedWeights: WeightRecord[] = []; - weightsData.forEach((petWeightGroup: any) => { - const petId = petWeightGroup.pet_id; - if (petWeightGroup.weights && Array.isArray(petWeightGroup.weights)) { - petWeightGroup.weights.forEach((weight: any) => { - flattenedWeights.push({ - ...weight, - petId: petId, - weight: parseFloat(weight.weight) // Convert string to number - }); - }); - } - }); - setWeightRecords(flattenedWeights); - } else { - // If flat structure - setWeightRecords(weightsData); - } - } + setWeightRecords(fetchedWeights); } catch (err: any) { console.error('Failed to fetch data:', err); setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); @@ -107,6 +90,8 @@ export default function WeightManagementScreen() { ); const handleOpenModal = () => { + setIsEditMode(false); + setEditingWeightId(null); setWeight(''); setDate(new Date().toISOString().split('T')[0]); setModalVisible(true); @@ -114,51 +99,57 @@ export default function WeightManagementScreen() { const handleCloseModal = () => { setModalVisible(false); + setIsEditMode(false); + setEditingWeightId(null); }; const handleSaveWeight = async () => { - if (!selectedPetId || !weight) { + if (!weight) { alert('Täytä kaikki pakolliset kentät'); return; } try { setSaving(true); - - const weightData = { - pet_id: selectedPetId, - weight: parseFloat(weight), - date: date - }; - const response = await apiClient.post('/api/weights', weightData); + if (isEditMode && editingWeightId) { - if (response.data.success) { - const weightsResponse = await apiClient.get('/api/weights'); - if (weightsResponse.data.success && weightsResponse.data.data) { - const weightsData = weightsResponse.data.data; + const weightData = { + pet_id: selectedPetId, + weight: parseFloat(weight), + date: date + }; + + const updatedWeight = await weightsService.updateWeight(editingWeightId, weightData); + + if (updatedWeight) { + // Refresh weights + const refreshedWeights = await weightsService.getAllWeights(); + setWeightRecords(refreshedWeights); + + handleCloseModal(); + } + } else { + if (!selectedPetId) { + alert('Valitse lemmikki ennen tallentamista.'); + return; + } + + const weightData = { + pet_id: selectedPetId, + weight: parseFloat(weight), + date: date + }; + + const newWeight = await weightsService.createWeight(weightData); + + if (newWeight) { + // Refresh weights + const refreshedWeights = await weightsService.getAllWeights(); + setWeightRecords(refreshedWeights); - if (Array.isArray(weightsData) && weightsData.length > 0 && weightsData[0].weights) { - const flattenedWeights: WeightRecord[] = []; - weightsData.forEach((petWeightGroup: any) => { - const petId = petWeightGroup.pet_id; - if (petWeightGroup.weights && Array.isArray(petWeightGroup.weights)) { - petWeightGroup.weights.forEach((weight: any) => { - flattenedWeights.push({ - ...weight, - petId: petId, - weight: parseFloat(weight.weight) - }); - }); - } - }); - setWeightRecords(flattenedWeights); - } else { - setWeightRecords(weightsData); - } + handleCloseModal(); } - - handleCloseModal(); } } catch (err: any) { alert('Painon tallentaminen epäonnistui. Yritä uudelleen.'); @@ -167,6 +158,43 @@ export default function WeightManagementScreen() { } }; + const handleEditWeightRecord = (weightRecord: WeightRecord) => { + setIsEditMode(true); + setEditingWeightId(weightRecord.id); + + // Pre-fill form with weight data + const dateOnly = weightRecord.date.split('T')[0]; + setWeight(weightRecord.weight.toString()); + setDate(dateOnly); + setModalVisible(true); + }; + + const handleDeleteWeightRecord = async (weightRecord: WeightRecord) => { + setWeightToDelete(weightRecord); + setDeleteDialogVisible(true); + }; + + const confirmDeleteWeightRecord = async () => { + if (!weightToDelete) return; + + try { + const success = await weightsService.deleteWeight(weightToDelete.id); + + if (success) { + // Refresh weights + const refreshedWeights = await weightsService.getAllWeights(); + setWeightRecords(refreshedWeights); + setDeleteDialogVisible(false); + setWeightToDelete(null); + } else { + alert('Painomittauksen poistaminen epäonnistui. Yritä uudelleen.'); + } + } catch (err: any) { + console.error("Failed to delete weight record:", err); + alert('Painomittauksen poistaminen epäonnistui. Yritä uudelleen.'); + } + }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -223,36 +251,29 @@ export default function WeightManagementScreen() { )} - - - - {formatDate(record.date)} - - + - {record.measuredBy && ( - <> - - - - - {record.measuredBy} + + {record.date ? ( + + + + {formatDate(record.date)} - - )} - - {record.notes && ( - <> - - - - - {record.notes} - + ) : ( + + )} + + + handleEditWeightRecord(record)} style={styles.actionButton}> + + + handleDeleteWeightRecord(record)} style={styles.actionButton}> + + - - )} + ); @@ -646,7 +667,7 @@ export default function WeightManagementScreen() { contentContainerStyle={styles.scrollContentContainer} > - Lisää painomittaus + {isEditMode ? 'Muokkaa painomittausta' : 'Lisää painomittaus'} setShowDatePicker(true)}> + setDeleteDialogVisible(false)} + > + Poista painomittaus + + Haluatko varmasti poistaa tämän painomittauksen? + + + + + + ); } -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: COLORS.background, - }, - 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: { - paddingBottom: 80, - }, - graphContainer: { - backgroundColor: COLORS.surface, - padding: SPACING.md, - paddingBottom: SPACING.xl, - marginHorizontal: SPACING.md, - marginTop: SPACING.md, - marginBottom: SPACING.md, - borderRadius: 12, - elevation: 2, - }, - graphTitle: { - fontWeight: '600', - color: COLORS.onSurface, - marginBottom: SPACING.md, - }, - graphContent: { - flexDirection: 'row', - }, - yAxisContainer: { - flexDirection: 'row', - alignItems: 'center', - }, - axisTitle: { - fontSize: 9, - color: COLORS.onSurfaceVariant, - fontWeight: '600', - }, - yAxisLabels: { - width: 50, - height: 220, - justifyContent: 'space-between', - alignItems: 'flex-end', - paddingRight: SPACING.xs, - marginTop: -50, - }, - yAxisMiddle: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - }, - axisLabel: { - color: COLORS.onSurfaceVariant, - textAlign: 'right', - lineHeight: 12, - }, - graphArea: { - flex: 1, - position: 'relative', - }, - gridLines: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - height: 220, - justifyContent: 'space-between', - }, - gridLine: { - height: 1, - backgroundColor: COLORS.surfaceVariant, - opacity: 0.5, - }, - svgGraph: { - marginBottom: SPACING.xs, - }, - xAxisLabelsContainer: { - flexDirection: 'row', - justifyContent: 'space-around', - paddingBottom: SPACING.sm, - position: 'relative', - height: 20, - }, - xAxisLabel: { - fontSize: 9, - color: COLORS.onSurfaceVariant, - textAlign: 'center', - flex: 1, - flexShrink: 1, - }, - xAxisLabelShown: { - fontSize: 10, - color: COLORS.onSurfaceVariant, - textAlign: 'center', - flex: 1, - }, - xAxisTitle: { - fontSize: 10, - color: COLORS.onSurfaceVariant, - fontWeight: '600', - textAlign: 'center', - marginTop: SPACING.xs, - }, - yearTabsContainer: { - marginTop: SPACING.md, - paddingTop: SPACING.sm, - borderTopWidth: 1, - borderTopColor: COLORS.surfaceVariant, - }, - yearTabsContent: { - gap: SPACING.xs, - paddingHorizontal: 4, - }, - yearTab: { - backgroundColor: COLORS.surfaceVariant, - }, - selectedYearTab: { - backgroundColor: COLORS.primary, - }, - selectedYearTabText: { - color: '#FFFFFF', - fontWeight: '600', - }, - unselectedYearTabText: { - color: COLORS.onSurfaceVariant, - }, - dividerSection: { - paddingHorizontal: SPACING.md, - }, - sectionTitle: { - fontWeight: '600', - color: COLORS.onSurface, - marginTop: SPACING.md, - marginBottom: SPACING.xs, - }, - weightCard: { - marginHorizontal: SPACING.md, - marginBottom: SPACING.md, - elevation: 2, - }, - weightHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING.sm, - }, - weightMainInfo: { - flexDirection: 'row', - alignItems: 'baseline', - gap: SPACING.xs, - }, - weightValue: { - fontWeight: '700', - color: COLORS.primary, - }, - weightUnit: { - color: COLORS.onSurfaceVariant, - fontWeight: '600', - }, - changeChip: { - marginLeft: SPACING.sm, - }, - increaseChip: { - backgroundColor: '#FFEBEE', - }, - decreaseChip: { - backgroundColor: '#E8F5E9', - }, - increaseChipText: { - color: '#D32F2F', - }, - decreaseChipText: { - color: '#2E7D32', - }, - dateContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.xs, - marginTop: SPACING.xs, - }, - dateText: { - color: COLORS.onSurfaceVariant, - }, - divider: { - marginVertical: SPACING.sm, - }, - weightDetail: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.sm, - marginBottom: SPACING.xs, - }, - detailText: { - flex: 1, - color: COLORS.onSurface, - }, - notesContainer: { - flexDirection: 'row', - gap: SPACING.sm, - marginTop: SPACING.xs, - }, - notesText: { - flex: 1, - color: COLORS.onSurfaceVariant, - fontStyle: 'italic', - }, - 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', - right: SPACING.md, - bottom: SPACING.md, - backgroundColor: COLORS.primary, - }, - graphScrollView: { - width: '100%', - }, - modalContainer: { - backgroundColor: COLORS.surface, - margin: SPACING.lg, - padding: SPACING.lg, - borderRadius: 12, - maxHeight: '90%', - }, - scrollContentContainer: { - paddingBottom: 0, - }, - modalTitle: { - marginBottom: SPACING.lg, - fontWeight: 'bold', - color: COLORS.onSurface, - }, - input: { - marginBottom: SPACING.md, - }, - modalButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: SPACING.lg, - gap: SPACING.md, - }, - modalButton: { - flex: 1, - }, -}); diff --git a/mobile/src/services/calendarService.ts b/mobile/src/services/calendarService.ts new file mode 100644 index 0000000..b2eb479 --- /dev/null +++ b/mobile/src/services/calendarService.ts @@ -0,0 +1,232 @@ +// Calendar service for managing calendar events +import apiClient from './api'; + +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; // Time before event to notify (e.g., '1 day', '1 hour') +} + +export interface CreateCalendarEventData { + pet_id: number; + title: string; + description?: string; + date: string; + event_type?: CalendarEvent['eventType']; + visit_type_id?: number; + completed?: boolean; + notification_enabled?: boolean; + notification_time?: string; +} + +export interface UpdateCalendarEventData { + pet_id?: number; + title?: string; + description?: string; + date?: string; + event_type?: CalendarEvent['eventType']; + completed?: boolean; + notification_enabled?: boolean; + notification_time?: string; +} + +export const calendarService = { + /** + * Get all calendar events + */ + async getAllEvents(): Promise { + 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/services/medicationsService.ts b/mobile/src/services/medicationsService.ts new file mode 100644 index 0000000..7defda7 --- /dev/null +++ b/mobile/src/services/medicationsService.ts @@ -0,0 +1,87 @@ +import apiClient from "./api"; + +interface Medication { + id: number; + pet_id: number; + med_name: string; + medication_date: string; + expire_date?: string; + notes?: string; + costs?: string; +} + +interface CreateMedicationData { + pet_id: number; + med_name: string; + medication_date: string; + expire_date?: string; + notes?: string; + costs?: number; +} + +interface UpdateMedicationData { + med_name?: string; + medication_date?: string; + expire_date?: string; + notes?: string; + costs?: number; +} + +export const medicationsService = { + // Get all medications + getAllMedications: async (): Promise => { + const response = await apiClient.get('/api/medications'); + + if (response.data.success && response.data.data) { + // Flatten the nested structure: each pet has a medications array + const flattenedMedications: Medication[] = []; + response.data.data.forEach((petMedicationGroup: any) => { + const petId = petMedicationGroup.pet_id; + if (petMedicationGroup.medications && Array.isArray(petMedicationGroup.medications)) { + petMedicationGroup.medications.forEach((medication: any) => { + flattenedMedications.push({ + ...medication, + pet_id: petId + }); + }); + } + }); + return flattenedMedications; + } + + return []; + }, + + // get medications for a specific pet + getMedicationsByPetId: async (petId: number): Promise => { + const allMedications = await medicationsService.getAllMedications(); + return allMedications.filter(medication => medication.pet_id === petId); + }, + + // Create a new medication + createMedication: async (medicationData: CreateMedicationData): Promise => { + const response = await apiClient.post('/api/medications', medicationData); + + if (response.data.success && response.data.data) { + return response.data.data; + } + return null; + }, + + // Update an existing medication + updateMedication: async (medicationId: number, updatedData: UpdateMedicationData): Promise => { + const response = await apiClient.put(`/api/medications/${medicationId}`, updatedData); + + if (response.data.success && response.data.data) { + return response.data.data; + } + + return null; + }, + + // Delete a medication + deleteMedication: async (medicationId: number): Promise => { + const response = await apiClient.delete(`/api/medications/${medicationId}`); + return response.data.success; + }, +}; \ No newline at end of file diff --git a/mobile/src/services/vaccinationsService.ts b/mobile/src/services/vaccinationsService.ts new file mode 100644 index 0000000..cbbfd4d --- /dev/null +++ b/mobile/src/services/vaccinationsService.ts @@ -0,0 +1,88 @@ +import apiClient from './api'; + +interface Vaccination { + id: number; + pet_id: number; + vac_name: string; + vaccination_date: string; + expire_date?: string; + costs?: string; + notes?: string; +} + +interface CreateVaccinationData { + pet_id: number; + vac_name: string; + vaccination_date: string; + expire_date?: string; + notes?: string; + costs?: number; +} + +interface UpdateVaccinationData { + vac_name?: string; + vaccination_date?: string; + expire_date?: string; + notes?: string; + costs?: number; +} + +export const vaccinationsService = { + // Get all vaccinations + getAllVaccinations: async (): Promise => { + const response = await apiClient.get('/api/vaccinations'); + + if (response.data.success && response.data.data) { + // Flatten the nested structure: each pet has a vaccinations array + const flattenedVaccinations: Vaccination[] = []; + response.data.data.forEach((petVaccinationGroup: any) => { + const petId = petVaccinationGroup.pet_id; + if (petVaccinationGroup.vaccinations && Array.isArray(petVaccinationGroup.vaccinations)) { + petVaccinationGroup.vaccinations.forEach((vaccination: any) => { + flattenedVaccinations.push({ + ...vaccination, + pet_id: petId + }); + }); + } + }); + return flattenedVaccinations; + } + + return []; + }, + + // Get vaccinations for a specific pet + getVaccinationsByPetId: async (petId: number): Promise => { + const allVaccinations = await vaccinationsService.getAllVaccinations(); + return allVaccinations.filter(vaccination => vaccination.pet_id === petId); + }, + + // Create a new vaccination + createVaccination: async (vaccinationData: CreateVaccinationData): Promise => { + const response = await apiClient.post('/api/vaccinations', vaccinationData); + + if (response.data.success) { + return response.data.data; + } + + return null; + }, + + // Update a vaccination + updateVaccination: async (vaccinationId: number, vaccinationData: UpdateVaccinationData): Promise => { + const response = await apiClient.put(`/api/vaccinations/${vaccinationId}`, vaccinationData); + + if (response.data.success) { + return response.data.data; + } + + return null; + }, + + // Delete a vaccination + deleteVaccination: async (vaccinationId: number): Promise => { + const response = await apiClient.delete(`/api/vaccinations/${vaccinationId}`); + return response.data.success; + }, +}; diff --git a/mobile/src/services/visitsService.ts b/mobile/src/services/visitsService.ts new file mode 100644 index 0000000..dcdbad7 --- /dev/null +++ b/mobile/src/services/visitsService.ts @@ -0,0 +1,107 @@ +import apiClient from './api'; + +interface Visit { + id: number; + pet_id: number; + visit_date: string; + location: string; + vet_name: string; + type_id: string; + notes?: string; + costs?: string; +} + +interface VisitType { + id: number; + name: string; +} + +interface CreateVisitData { + pet_id: number; + visit_date: string; + vet_name: string; + location: string; + type_id: number; + notes?: string; + costs?: number; +} + +interface UpdateVisitData { + visit_date?: string; + vet_name?: string; + location?: string; + type_id?: number; + notes?: string; + costs?: number; +} + +export const visitsService = { + // Get all visits + getAllVisits: async (): Promise => { + const response = await apiClient.get('/api/vet-visits'); + + if (response.data.success && response.data.data) { + // Flatten the nested structure: each pet has a vet_visits array + const flattenedVisits: Visit[] = []; + response.data.data.forEach((petVisitGroup: any) => { + const petId = petVisitGroup.pet_id; + if (petVisitGroup.vet_visits && Array.isArray(petVisitGroup.vet_visits)) { + petVisitGroup.vet_visits.forEach((visit: any) => { + flattenedVisits.push({ + ...visit, + pet_id: petId + }); + }); + } + }); + return flattenedVisits; + } + + return []; + }, + + // Get visits for a specific pet + getVisitsByPetId: async (petId: number): Promise => { + const allVisits = await visitsService.getAllVisits(); + return allVisits.filter(visit => visit.pet_id === petId); + }, + + // Get visit types + getVisitTypes: async (): Promise => { + const response = await apiClient.get('/api/vet-visits/types'); + + if (response.data.success && response.data.data) { + return response.data.data; + } + + return []; + }, + + // Create a new visit + createVisit: async (visitData: CreateVisitData): Promise => { + const response = await apiClient.post('/api/vet-visits', visitData); + + if (response.data.success) { + return response.data.data; + } + + return null; + }, + + // Update a visit + updateVisit: async (visitId: number, visitData: UpdateVisitData): Promise => { + const response = await apiClient.put(`/api/vet-visits/${visitId}`, visitData); + + if (response.data.success) { + return response.data.data; + } + + return null; + }, + + // Delete a visit + deleteVisit: async (visitId: number): Promise => { + const response = await apiClient.delete(`/api/vet-visits/${visitId}`); + return response.data.success; + }, +}; diff --git a/mobile/src/services/weightsService.ts b/mobile/src/services/weightsService.ts new file mode 100644 index 0000000..6a50537 --- /dev/null +++ b/mobile/src/services/weightsService.ts @@ -0,0 +1,94 @@ +import apiClient from './api'; + +interface WeightRecord { + id: number; + petId: number; + date: string; + weight: number; + created_at: string; + notes?: string; + measuredBy?: string; +} + +interface CreateWeightData { + pet_id: number; + weight: number; + date: string; + notes?: string; + measuredBy?: string; +} + +interface UpdateWeightData { + weight?: number; + date?: string; + notes?: string; + measuredBy?: string; +} + +export const weightsService = { + // Get all weight records + getAllWeights: async (): Promise => { + const response = await apiClient.get('/api/weights'); + + if (response.data.success && response.data.data) { + const weightsData = response.data.data; + + // If nested structure (array of pet weight groups) + if (Array.isArray(weightsData) && weightsData.length > 0 && weightsData[0].weights) { + const flattenedWeights: WeightRecord[] = []; + weightsData.forEach((petWeightGroup: any) => { + const petId = petWeightGroup.pet_id; + if (petWeightGroup.weights && Array.isArray(petWeightGroup.weights)) { + petWeightGroup.weights.forEach((weight: any) => { + flattenedWeights.push({ + ...weight, + petId: petId, + weight: parseFloat(weight.weight) // Convert string to number + }); + }); + } + }); + return flattenedWeights; + } else { + // If flat structure + return weightsData; + } + } + + return []; + }, + + // Get weight records for a specific pet + getWeightsByPetId: async (petId: number): Promise => { + const allWeights = await weightsService.getAllWeights(); + return allWeights.filter(weight => weight.petId === petId); + }, + + // Create a new weight record + createWeight: async (weightData: CreateWeightData): Promise => { + const response = await apiClient.post('/api/weights', weightData); + + if (response.data.success) { + return response.data.data; + } + + return null; + }, + + // Update a weight record + updateWeight: async (weightId: number, weightData: UpdateWeightData): Promise => { + const response = await apiClient.put(`/api/weights/${weightId}`, weightData); + + if (response.data.success) { + return response.data.data; + } + + return null; + }, + + // Delete a weight record + deleteWeight: async (weightId: number): Promise => { + const response = await apiClient.delete(`/api/weights/${weightId}`); + return response.data.success; + }, +}; diff --git a/mobile/src/styles/authStyles.ts b/mobile/src/styles/authStyles.ts index 1f1317c..569fd2a 100644 --- a/mobile/src/styles/authStyles.ts +++ b/mobile/src/styles/authStyles.ts @@ -78,6 +78,7 @@ export const authStyles = StyleSheet.create({ ...COMMON_STYLES.buttonContent, }, + // Links forgotPassword: { ...TYPOGRAPHY.bodyMedium, diff --git a/mobile/src/styles/screenStyles.ts b/mobile/src/styles/screenStyles.ts index 1915a9c..8f3c75a 100644 --- a/mobile/src/styles/screenStyles.ts +++ b/mobile/src/styles/screenStyles.ts @@ -234,6 +234,1337 @@ export const profileStyles = StyleSheet.create({ }, logoutButton: { - borderRadius: COMMON_STYLES.button.borderRadius, + marginBottom: 0, }, }); + +// ============================================ +// VISITS SCREEN STYLES +// ============================================ +export const visitsStyles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.background, + }, + + header: { + paddingHorizontal: SPACING.lg, + paddingTop: SPACING.lg, + paddingBottom: SPACING.md, + }, + + title: { + fontWeight: 'bold', + color: COLORS.onBackground, + }, + + 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, + }, + + visitCard: { + marginBottom: SPACING.md, + elevation: 2, + }, + + visitHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: SPACING.sm, + }, + + dateContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: SPACING.xs, + }, + + dateText: { + fontWeight: '600', + color: COLORS.primary, + }, + + divider: { + marginVertical: SPACING.sm, + }, + + visitDetail: { + flexDirection: 'row', + alignItems: 'center', + gap: SPACING.sm, + marginBottom: SPACING.xs, + }, + + detailText: { + flex: 1, + color: COLORS.onSurface, + }, + + bottomSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: SPACING.sm, + }, + + notesContainer: { + flexDirection: 'row', + gap: SPACING.sm, + flex: 1, + alignItems: 'flex-start', + }, + + notesText: { + flex: 1, + color: COLORS.onSurfaceVariant, + fontStyle: 'italic', + }, + + cardActions: { + marginTop: SPACING.md, + alignItems: 'flex-end', + }, + + actionButtons: { + flexDirection: 'row', + gap: SPACING.sm, + }, + + actionButton: { + padding: SPACING.xs, + borderRadius: 8, + backgroundColor: COLORS.surfaceVariant, + justifyContent: 'center' as const, + alignItems: 'center' as const, + }, + + 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, + }, + + modalContainer: { + backgroundColor: COLORS.surface, + margin: SPACING.lg, + padding: SPACING.lg, + borderRadius: 12, + maxHeight: '90%', + }, + + keyboardAvoid: { + width: '100%', + }, + + scrollContentContainer: { + paddingBottom: 0, + }, + + modalTitle: { + marginBottom: SPACING.lg, + fontWeight: 'bold', + color: COLORS.onSurface, + }, + + input: { + marginBottom: SPACING.md, + }, + + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: SPACING.lg, + gap: SPACING.md, + }, + + modalButton: { + flex: 1, + }, +}); + +// ============================================ +// MEDICATIONS SCREEN STYLES +// ============================================ +export const medicationsStyles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.background, + }, + + 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, + }, + + medicationCard: { + marginBottom: SPACING.md, + elevation: 2, + }, + + medicationHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: SPACING.sm, + }, + + medicationName: { + fontWeight: '700', + color: COLORS.primary, + flex: 1, + }, + + dosageContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: SPACING.xs, + marginBottom: SPACING.sm, + }, + + dosage: { + fontWeight: '600', + color: COLORS.onSurface, + }, + + divider: { + marginVertical: SPACING.sm, + }, + + bottomSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: SPACING.sm, + }, + + notesContainer: { + flexDirection: 'row', + gap: SPACING.sm, + flex: 1, + alignItems: 'flex-start', + }, + + notesText: { + flex: 1, + color: COLORS.onSurfaceVariant, + fontStyle: 'italic', + }, + + cardActions: { + marginTop: SPACING.md, + alignItems: 'flex-end', + }, + + actionButtons: { + flexDirection: 'row', + gap: SPACING.sm, + }, + + actionButton: { + padding: SPACING.xs, + borderRadius: 8, + backgroundColor: COLORS.surfaceVariant, + justifyContent: 'center' as const, + alignItems: 'center' as const, + }, + + 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, + }, + + modalContainer: { + backgroundColor: COLORS.surface, + margin: SPACING.lg, + padding: SPACING.lg, + borderRadius: 12, + maxHeight: '90%', + }, + + scrollContentContainer: { + paddingBottom: 0, + }, + + modalTitle: { + marginBottom: SPACING.lg, + fontWeight: 'bold', + color: COLORS.onSurface, + }, + + input: { + marginBottom: SPACING.md, + }, + + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: SPACING.lg, + gap: SPACING.md, + }, + + modalButton: { + flex: 1, + }, +}); + +// ============================================ +// VACCINATIONS SCREEN STYLES +// ============================================ +export const vaccinationsStyles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.background, + }, + + 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, + }, + + vaccinationCard: { + marginBottom: SPACING.md, + elevation: 2, + }, + + vaccinationHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: SPACING.sm, + }, + + vaccinationName: { + fontWeight: '700', + color: COLORS.primary, + flex: 1, + }, + + dateContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: SPACING.xs, + marginBottom: SPACING.sm, + }, + + dateText: { + fontWeight: '600', + color: COLORS.onSurface, + }, + + divider: { + marginVertical: SPACING.sm, + }, + + bottomSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: SPACING.sm, + }, + + notesContainer: { + flexDirection: 'row', + gap: SPACING.sm, + flex: 1, + alignItems: 'flex-start', + }, + + notesText: { + flex: 1, + color: COLORS.onSurfaceVariant, + fontStyle: 'italic', + }, + + cardActions: { + marginTop: SPACING.md, + alignItems: 'flex-end', + }, + + actionButtons: { + flexDirection: 'row', + gap: SPACING.sm, + }, + + actionButton: { + padding: SPACING.xs, + borderRadius: 8, + backgroundColor: COLORS.surfaceVariant, + justifyContent: 'center' as const, + alignItems: 'center' as const, + }, + + 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, + }, + + modalContainer: { + backgroundColor: COLORS.surface, + margin: SPACING.lg, + padding: SPACING.lg, + borderRadius: 12, + maxHeight: '90%', + }, + + scrollContentContainer: { + paddingBottom: 0, + }, + + modalTitle: { + marginBottom: SPACING.lg, + fontWeight: 'bold', + color: COLORS.onSurface, + }, + + input: { + marginBottom: SPACING.md, + }, + + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: SPACING.lg, + gap: SPACING.md, + }, + + modalButton: { + flex: 1, + }, +}); + +// ============================================ +// WEIGHTS SCREEN STYLES +// ============================================ +export const weightsStyles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.background, + }, + + 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: { + paddingBottom: 80, + }, + + graphContainer: { + backgroundColor: COLORS.surface, + padding: SPACING.md, + paddingBottom: SPACING.xl, + marginHorizontal: SPACING.md, + marginTop: SPACING.md, + marginBottom: SPACING.md, + borderRadius: 12, + elevation: 2, + }, + + graphTitle: { + fontWeight: '600', + color: COLORS.onSurface, + marginBottom: SPACING.md, + }, + + graphContent: { + flexDirection: 'row', + }, + + yAxisContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + + axisTitle: { + fontSize: 9, + color: COLORS.onSurfaceVariant, + fontWeight: '600', + }, + + yAxisLabels: { + width: 50, + height: 220, + justifyContent: 'space-between', + alignItems: 'flex-end', + paddingRight: SPACING.xs, + marginTop: -50, + }, + + yAxisMiddle: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + + axisLabel: { + color: COLORS.onSurfaceVariant, + textAlign: 'right', + lineHeight: 12, + }, + + graphArea: { + flex: 1, + position: 'relative', + }, + + gridLines: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: 220, + justifyContent: 'space-between', + }, + + gridLine: { + height: 1, + backgroundColor: COLORS.surfaceVariant, + opacity: 0.5, + }, + + svgGraph: { + marginBottom: SPACING.xs, + }, + + xAxisLabelsContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + paddingBottom: SPACING.sm, + position: 'relative', + height: 20, + }, + + xAxisLabel: { + fontSize: 9, + color: COLORS.onSurfaceVariant, + textAlign: 'center', + flex: 1, + flexShrink: 1, + }, + + xAxisLabelShown: { + fontSize: 10, + color: COLORS.onSurfaceVariant, + textAlign: 'center', + flex: 1, + }, + + xAxisTitle: { + fontSize: 10, + color: COLORS.onSurfaceVariant, + fontWeight: '600', + textAlign: 'center', + marginTop: SPACING.xs, + }, + + yearTabsContainer: { + marginTop: SPACING.md, + paddingTop: SPACING.sm, + borderTopWidth: 1, + borderTopColor: COLORS.surfaceVariant, + }, + + yearTabsContent: { + gap: SPACING.xs, + paddingHorizontal: 4, + }, + + yearTab: { + backgroundColor: COLORS.surfaceVariant, + }, + + selectedYearTab: { + backgroundColor: COLORS.primary, + }, + + selectedYearTabText: { + color: '#FFFFFF', + fontWeight: '600', + }, + + unselectedYearTabText: { + color: COLORS.onSurfaceVariant, + }, + + dividerSection: { + paddingHorizontal: SPACING.md, + }, + + sectionTitle: { + fontWeight: '600', + color: COLORS.onSurface, + marginTop: SPACING.md, + marginBottom: SPACING.xs, + }, + + weightCard: { + marginHorizontal: SPACING.md, + marginBottom: SPACING.md, + elevation: 2, + }, + + weightHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: SPACING.sm, + }, + + weightMainInfo: { + flexDirection: 'row', + alignItems: 'baseline', + gap: SPACING.xs, + }, + + weightValue: { + fontWeight: '700', + color: COLORS.primary, + }, + + weightUnit: { + color: COLORS.onSurfaceVariant, + fontWeight: '600', + }, + + changeChip: { + marginLeft: SPACING.sm, + }, + + increaseChip: { + backgroundColor: '#FFEBEE', + }, + + decreaseChip: { + backgroundColor: '#E8F5E9', + }, + + increaseChipText: { + color: '#D32F2F', + }, + + decreaseChipText: { + color: '#2E7D32', + }, + + dateContainer: { + flexDirection: 'row', + flex: 1, + alignItems: 'flex-start', + gap: SPACING.xs, + marginTop: SPACING.xs, + }, + + dateText: { + color: COLORS.onSurfaceVariant, + }, + + divider: { + marginVertical: SPACING.sm, + }, + + weightDetail: { + flexDirection: 'row', + alignItems: 'center', + gap: SPACING.sm, + marginBottom: SPACING.xs, + }, + + detailText: { + flex: 1, + color: COLORS.onSurface, + }, + + bottomSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: SPACING.sm, + }, + + notesContainer: { + flexDirection: 'row', + gap: SPACING.sm, + flex: 1, + alignItems: 'flex-start', + }, + + notesText: { + flex: 1, + color: COLORS.onSurfaceVariant, + fontStyle: 'italic', + }, + + cardActions: { + marginTop: SPACING.md, + alignItems: 'flex-end', + }, + + actionButtons: { + flexDirection: 'row', + gap: SPACING.sm, + }, + + actionButton: { + padding: SPACING.xs, + borderRadius: 8, + backgroundColor: COLORS.surfaceVariant, + justifyContent: 'center' as const, + alignItems: 'center' as const, + }, + + 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, + }, + + graphScrollView: { + width: '100%', + }, + + modalContainer: { + backgroundColor: COLORS.surface, + margin: SPACING.lg, + padding: SPACING.lg, + borderRadius: 12, + maxHeight: '90%', + }, + + scrollContentContainer: { + paddingBottom: 0, + }, + + modalTitle: { + marginBottom: SPACING.lg, + fontWeight: 'bold', + color: COLORS.onSurface, + }, + + input: { + marginBottom: SPACING.md, + }, + + modalButtons: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: SPACING.lg, + gap: SPACING.md, + }, + + 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/styles/theme.ts b/mobile/src/styles/theme.ts index dcfee10..97dd53a 100644 --- a/mobile/src/styles/theme.ts +++ b/mobile/src/styles/theme.ts @@ -285,8 +285,8 @@ export const COMMON_STYLES = { logoContainer: { width: 120, height: 120, - justifyContent: 'center', - alignItems: 'center', + justifyContent: 'center' as const, + alignItems: 'center' as const, backgroundColor: COLORS.surfaceVariant, borderRadius: BORDER_RADIUS.large, }, diff --git a/mobile/src/types/index.ts b/mobile/src/types/index.ts index 64ae941..82a83c3 100644 --- a/mobile/src/types/index.ts +++ b/mobile/src/types/index.ts @@ -91,3 +91,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; +}