From 1a72e993a6babdb94ab6b856b72e0ac0303650ab Mon Sep 17 00:00:00 2001 From: Samu Date: Tue, 27 Jan 2026 15:50:58 +0200 Subject: [PATCH 1/8] display pets in health section tabs --- mobile/src/screens/MedicationsScreen.tsx | 77 +++++++++++++++--- mobile/src/screens/VaccinationsScreen.tsx | 77 +++++++++++++++--- mobile/src/screens/VisitsScreen.tsx | 77 +++++++++++++++--- mobile/src/screens/WeightManagementScreen.tsx | 79 +++++++++++++++---- 4 files changed, 257 insertions(+), 53 deletions(-) diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index d3713a7..15b199e 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -1,15 +1,10 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, ScrollView, StyleSheet } from 'react-native'; -import { Text, Card, FAB, Chip, Divider } from 'react-native-paper'; +import { Text, Card, FAB, Chip, Divider, ActivityIndicator } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { COLORS, SPACING } from '../styles/theme'; - -interface Pet { - id: string; - name: string; - breed: string; - age: number; -} +import apiClient from '../services/api'; +import { Pet } from '../types'; interface Medication { id: string; @@ -26,12 +21,39 @@ interface Medication { } export default function MedicationsScreen() { - // Mock data - replace with actual data from context/API - const [pets] = useState([]); - + const [pets, setPets] = useState([]); const [medications] = useState([]); + const [selectedPetId, setSelectedPetId] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch pets from the API + useEffect(() => { + const fetchPets = async () => { + try { + setLoading(true); + setError(null); + const response = await apiClient.get('/api/pets'); + + if (response.data.success && response.data.data) { + const fetchedPets = response.data.data; + setPets(fetchedPets); + + // Set the first pet as selected by default + if (fetchedPets.length > 0) { + setSelectedPetId(fetchedPets[0].id); + } + } + } catch (err: any) { + console.error('Failed to fetch pets:', err); + setError('Lemmikit eivät latautuneet. Yritä uudelleen.'); + } finally { + setLoading(false); + } + }; - const [selectedPetId, setSelectedPetId] = useState(pets[0]?.id || ''); + fetchPets(); + }, []); const selectedPetMedications = medications .filter(med => med.petId === selectedPetId) @@ -137,6 +159,35 @@ export default function MedicationsScreen() { ); + if (loading) { + return ( + + + + + Ladataan lemmikkejä... + + + + ); + } + + if (error) { + return ( + + + + + Virhe + + + {error} + + + + ); + } + if (pets.length === 0) { return ( diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index b6c3863..62bd413 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -1,15 +1,10 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, ScrollView, StyleSheet } from 'react-native'; -import { Text, Card, FAB, Chip, Divider } from 'react-native-paper'; +import { Text, Card, FAB, Chip, Divider, ActivityIndicator } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { COLORS, SPACING } from '../styles/theme'; - -interface Pet { - id: string; - name: string; - breed: string; - age: number; -} +import apiClient from '../services/api'; +import { Pet } from '../types'; interface Vaccination { id: string; @@ -25,12 +20,39 @@ interface Vaccination { } export default function VaccinationsScreen() { - // Mock data - replace with actual data from context/API - const [pets] = useState([]); - + const [pets, setPets] = useState([]); const [vaccinations] = useState([]); + const [selectedPetId, setSelectedPetId] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch pets from the API + useEffect(() => { + const fetchPets = async () => { + try { + setLoading(true); + setError(null); + const response = await apiClient.get('/api/pets'); + + if (response.data.success && response.data.data) { + const fetchedPets = response.data.data; + setPets(fetchedPets); + + // Set the first pet as selected by default + if (fetchedPets.length > 0) { + setSelectedPetId(fetchedPets[0].id); + } + } + } catch (err: any) { + console.error('Failed to fetch pets:', err); + setError('Lemmikit eivät latautuneet. Yritä uudelleen.'); + } finally { + setLoading(false); + } + }; - const [selectedPetId, setSelectedPetId] = useState(pets[0]?.id || ''); + fetchPets(); + }, []); const selectedPetVaccinations = vaccinations .filter(vac => vac.petId === selectedPetId) @@ -147,6 +169,35 @@ export default function VaccinationsScreen() { ); + if (loading) { + return ( + + + + + Ladataan lemmikkejä... + + + + ); + } + + if (error) { + return ( + + + + + Virhe + + + {error} + + + + ); + } + if (pets.length === 0) { return ( diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index ffc7547..d23c327 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -1,15 +1,10 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, ScrollView, StyleSheet } from 'react-native'; -import { Text, Card, FAB, Chip, Divider } from 'react-native-paper'; +import { Text, Card, FAB, Chip, Divider, ActivityIndicator } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { COLORS, SPACING } from '../styles/theme'; - -interface Pet { - id: string; - name: string; - breed: string; - age: number; -} +import apiClient from '../services/api'; +import { Pet } from '../types'; interface Visit { id: string; @@ -23,12 +18,39 @@ interface Visit { } export default function VisitsScreen() { - // Mock data - replace with actual data from context/API - const [pets] = useState([]); - + const [pets, setPets] = useState([]); const [visits] = useState([]); + const [selectedPetId, setSelectedPetId] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const [selectedPetId, setSelectedPetId] = useState(pets[0]?.id || ''); + // Fetch pets from the API + useEffect(() => { + const fetchPets = async () => { + try { + setLoading(true); + setError(null); + const response = await apiClient.get('/api/pets'); + + if (response.data.success && response.data.data) { + const fetchedPets = response.data.data; + setPets(fetchedPets); + + // Set the first pet as selected by default + if (fetchedPets.length > 0) { + setSelectedPetId(fetchedPets[0].id); + } + } + } catch (err: any) { + console.error('Failed to fetch pets:', err); + setError('Lemmikit eivät latautuneet. Yritä uudelleen.'); + } finally { + setLoading(false); + } + }; + + fetchPets(); + }, []); const selectedPetVisits = visits .filter(visit => visit.petId === selectedPetId) @@ -110,6 +132,35 @@ export default function VisitsScreen() { ); + if (loading) { + return ( + + + + + Ladataan lemmikkejä... + + + + ); + } + + if (error) { + return ( + + + + + Virhe + + + {error} + + + + ); + } + if (pets.length === 0) { return ( diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx index c912bec..5a3913a 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -1,16 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, ScrollView, StyleSheet, Dimensions } from 'react-native'; -import { Text, Card, FAB, Chip, Divider } from 'react-native-paper'; +import { Text, Card, FAB, Chip, Divider, ActivityIndicator } from 'react-native-paper'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import Svg, { Line, Circle } from 'react-native-svg'; import { COLORS, SPACING } from '../styles/theme'; - -interface Pet { - id: string; - name: string; - breed: string; - age: number; -} +import apiClient from '../services/api'; +import { Pet } from '../types'; interface WeightRecord { id: string; @@ -23,13 +18,40 @@ interface WeightRecord { } export default function WeightManagementScreen() { - // Mock data - replace with actual data from context/API - const [pets] = useState([]); - + const [pets, setPets] = useState([]); const [weightRecords] = useState([]); - - const [selectedPetId, setSelectedPetId] = useState(pets[0]?.id || ''); + const [selectedPetId, setSelectedPetId] = useState(''); const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch pets from the API + useEffect(() => { + const fetchPets = async () => { + try { + setLoading(true); + setError(null); + const response = await apiClient.get('/api/pets'); + + if (response.data.success && response.data.data) { + const fetchedPets = response.data.data; + setPets(fetchedPets); + + // Set the first pet as selected by default + if (fetchedPets.length > 0) { + setSelectedPetId(fetchedPets[0].id); + } + } + } catch (err: any) { + console.error('Failed to fetch pets:', err); + setError('Lemmikit eivät latautuneet. Yritä uudelleen.'); + } finally { + setLoading(false); + } + }; + + fetchPets(); + }, []); const selectedPetWeights = weightRecords .filter(record => record.petId === selectedPetId) .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); @@ -143,6 +165,35 @@ export default function WeightManagementScreen() { ); + if (loading) { + return ( + + + + + Ladataan lemmikkejä... + + + + ); + } + + if (error) { + return ( + + + + + Virhe + + + {error} + + + + ); + } + if (pets.length === 0) { return ( From c7d7d614be42561f89b3d465d26ef0bee5bfeec4 Mon Sep 17 00:00:00 2001 From: Samu Date: Tue, 27 Jan 2026 16:14:18 +0200 Subject: [PATCH 2/8] Pet visits now display correctly --- mobile/src/screens/MedicationsScreen.tsx | 4 +- mobile/src/screens/VaccinationsScreen.tsx | 4 +- mobile/src/screens/VisitsScreen.tsx | 73 +++++++++++++++-------- mobile/src/types/index.ts | 2 +- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index 15b199e..e391fd2 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -8,7 +8,7 @@ import { Pet } from '../types'; interface Medication { id: string; - petId: string; + petId: number; name: string; dosage: string; frequency: string; @@ -23,7 +23,7 @@ interface Medication { export default function MedicationsScreen() { const [pets, setPets] = useState([]); const [medications] = useState([]); - const [selectedPetId, setSelectedPetId] = useState(''); + const [selectedPetId, setSelectedPetId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index 62bd413..0ed0ce1 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -8,7 +8,7 @@ import { Pet } from '../types'; interface Vaccination { id: string; - petId: string; + petId: number; name: string; date: string; nextDueDate?: string; @@ -22,7 +22,7 @@ interface Vaccination { export default function VaccinationsScreen() { const [pets, setPets] = useState([]); const [vaccinations] = useState([]); - const [selectedPetId, setSelectedPetId] = useState(''); + const [selectedPetId, setSelectedPetId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index d23c327..1457c3e 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -7,33 +7,38 @@ import apiClient from '../services/api'; import { Pet } from '../types'; interface Visit { - id: string; - petId: string; - date: string; - clinic: string; - veterinarian: string; - reason: string; + id: number; + pet_id: number; + visit_date: string; + location: string; + vet_name: string; + type_id: string; notes?: string; - cost?: number; + costs?: string; } export default function VisitsScreen() { const [pets, setPets] = useState([]); - const [visits] = useState([]); - const [selectedPetId, setSelectedPetId] = useState(''); + const [visits, setVisits] = useState([]); + const [selectedPetId, setSelectedPetId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Fetch pets from the API + // Fetch pets and visits from the API useEffect(() => { - const fetchPets = async () => { + const fetchData = async () => { try { setLoading(true); setError(null); - const response = await apiClient.get('/api/pets'); - if (response.data.success && response.data.data) { - const fetchedPets = response.data.data; + // Fetch both pets and visits in parallel + const [petsResponse, visitsResponse] = await Promise.all([ + apiClient.get('/api/pets'), + apiClient.get('/api/vet-visits') + ]); + + if (petsResponse.data.success && petsResponse.data.data) { + const fetchedPets = petsResponse.data.data; setPets(fetchedPets); // Set the first pet as selected by default @@ -41,21 +46,37 @@ export default function VisitsScreen() { setSelectedPetId(fetchedPets[0].id); } } + + 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); + } } catch (err: any) { - console.error('Failed to fetch pets:', err); - setError('Lemmikit eivät latautuneet. Yritä uudelleen.'); + console.error('Failed to fetch data:', err); + setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { setLoading(false); } }; - fetchPets(); + fetchData(); }, []); const selectedPetVisits = visits - .filter(visit => visit.petId === selectedPetId) - .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - + .filter(visit => visit.pet_id === selectedPetId) + .sort((a, b) => new Date(b.visit_date).getTime() - new Date(a.visit_date).getTime()); const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -72,12 +93,12 @@ export default function VisitsScreen() { - {formatDate(visit.date)} + {formatDate(visit.visit_date)} - {visit.cost && ( + {visit.costs && ( - {visit.cost} € + {visit.costs} € )} @@ -87,21 +108,21 @@ export default function VisitsScreen() { - {visit.clinic} + {visit.location} - {visit.veterinarian} + {visit.vet_name} - {visit.reason} + {visit.type_id} diff --git a/mobile/src/types/index.ts b/mobile/src/types/index.ts index 7573a81..56c3539 100644 --- a/mobile/src/types/index.ts +++ b/mobile/src/types/index.ts @@ -1,6 +1,6 @@ // Type definitions for the application export interface Pet { - id: string; + id: number; name: string; breed: string; age: number; From a936fae166758b35fb187e3907dbf99e35a3e869 Mon Sep 17 00:00:00 2001 From: Samu Date: Tue, 27 Jan 2026 16:19:27 +0200 Subject: [PATCH 3/8] displays medications correctly --- mobile/src/screens/MedicationsScreen.tsx | 188 ++++++++++++----------- 1 file changed, 99 insertions(+), 89 deletions(-) diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index e391fd2..e3de4b1 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -7,36 +7,37 @@ import apiClient from '../services/api'; import { Pet } from '../types'; interface Medication { - id: string; + id: number; petId: number; - name: string; - dosage: string; - frequency: string; - startDate: string; - endDate?: string; - purpose: string; - prescribedBy: string; + med_name: string; + medication_date: string; + expire_date?: string; + costs?: string; notes?: string; - isActive: boolean; } export default function MedicationsScreen() { const [pets, setPets] = useState([]); - const [medications] = useState([]); + const [medications, setMedications] = useState([]); const [selectedPetId, setSelectedPetId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Fetch pets from the API + // Fetch pets and medications from the API useEffect(() => { - const fetchPets = async () => { + const fetchData = async () => { try { setLoading(true); setError(null); - const response = await apiClient.get('/api/pets'); - if (response.data.success && response.data.data) { - const fetchedPets = response.data.data; + // Fetch both pets and medications in parallel + const [petsResponse, medicationsResponse] = await Promise.all([ + apiClient.get('/api/pets'), + apiClient.get('/api/medications') + ]); + + if (petsResponse.data.success && petsResponse.data.data) { + const fetchedPets = petsResponse.data.data; setPets(fetchedPets); // Set the first pet as selected by default @@ -44,24 +45,48 @@ export default function MedicationsScreen() { setSelectedPetId(fetchedPets[0].id); } } + + if (medicationsResponse.data.success && medicationsResponse.data.data) { + // Check if medications are nested like visits + const medicationsData = medicationsResponse.data.data; + console.log('Medications response:', medicationsData); + + // If nested structure (array of pet medication groups) + 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, + petId: petId + }); + }); + } + }); + setMedications(flattenedMedications); + } else { + // If flat structure + setMedications(medicationsData); + } + } } catch (err: any) { - console.error('Failed to fetch pets:', err); - setError('Lemmikit eivät latautuneet. Yritä uudelleen.'); + console.error('Failed to fetch data:', err); + setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { setLoading(false); } }; - fetchPets(); + fetchData(); }, []); const selectedPetMedications = medications .filter(med => med.petId === selectedPetId) .sort((a, b) => { - if (a.isActive === b.isActive) { - return new Date(b.startDate).getTime() - new Date(a.startDate).getTime(); - } - return a.isActive ? -1 : 1; + // Sort by medication date, newest first + return new Date(b.medication_date).getTime() - new Date(a.medication_date).getTime(); }); const formatDate = (dateString: string) => { @@ -73,79 +98,64 @@ export default function MedicationsScreen() { }); }; - const renderMedicationCard = (medication: Medication) => ( - - - - - {medication.name} - - - {medication.isActive ? 'Aktiivinen' : 'Päättynyt'} - - - - - - - {medication.dosage} - {medication.frequency} - - - - - - - - - {medication.purpose} - - - - - - - {medication.prescribedBy} - - - - - - - Aloitettu: {formatDate(medication.startDate)} - - + const renderMedicationCard = (medication: Medication) => { + const isExpired = medication.expire_date ? new Date(medication.expire_date) < new Date() : false; + + return ( + + + + + {medication.med_name} + + {medication.costs && ( + + {medication.costs} € + + )} + - {medication.endDate && ( - - - - Päättynyt: {formatDate(medication.endDate)} + + + + Aloitettu: {formatDate(medication.medication_date)} - )} - {medication.notes && ( - <> - - - - - {medication.notes} + {medication.expire_date && ( + + + + Vanhenee: {formatDate(medication.expire_date)} - - )} - - - ); + )} + + {medication.notes && ( + <> + + + + + {medication.notes} + + + + )} + + + ); + }; const renderEmptyState = () => ( From 1b19f4d997abf52f08a69b58e2d70b01a5b17b6a Mon Sep 17 00:00:00 2001 From: Samu Date: Tue, 27 Jan 2026 16:23:26 +0200 Subject: [PATCH 4/8] displays vaccinations correctly --- mobile/src/screens/VaccinationsScreen.tsx | 117 +++++++++++----------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index 0ed0ce1..cecd930 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -7,35 +7,37 @@ import apiClient from '../services/api'; import { Pet } from '../types'; interface Vaccination { - id: string; + id: number; petId: number; - name: string; - date: string; - nextDueDate?: string; - batchNumber?: string; - veterinarian: string; - clinic: string; + vac_name: string; + vaccination_date: string; + expire_date?: string; + costs?: string; notes?: string; - isUpToDate: boolean; } export default function VaccinationsScreen() { const [pets, setPets] = useState([]); - const [vaccinations] = useState([]); + const [vaccinations, setVaccinations] = useState([]); const [selectedPetId, setSelectedPetId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Fetch pets from the API + // Fetch pets and vaccinations from the API useEffect(() => { - const fetchPets = async () => { + const fetchData = async () => { try { setLoading(true); setError(null); - const response = await apiClient.get('/api/pets'); - if (response.data.success && response.data.data) { - const fetchedPets = response.data.data; + // Fetch both pets and vaccinations in parallel + const [petsResponse, vaccinationsResponse] = await Promise.all([ + apiClient.get('/api/pets'), + apiClient.get('/api/vaccinations') + ]); + + if (petsResponse.data.success && petsResponse.data.data) { + const fetchedPets = petsResponse.data.data; setPets(fetchedPets); // Set the first pet as selected by default @@ -43,20 +45,44 @@ export default function VaccinationsScreen() { setSelectedPetId(fetchedPets[0].id); } } + + if (vaccinationsResponse.data.success && vaccinationsResponse.data.data) { + const vaccinationsData = vaccinationsResponse.data.data; + + // If nested structure (array of pet vaccination groups) + 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, + petId: petId + }); + }); + } + }); + setVaccinations(flattenedVaccinations); + } else { + // If flat structure + setVaccinations(vaccinationsData); + } + } } catch (err: any) { - console.error('Failed to fetch pets:', err); - setError('Lemmikit eivät latautuneet. Yritä uudelleen.'); + console.error('Failed to fetch data:', err); + setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { setLoading(false); } }; - fetchPets(); + fetchData(); }, []); const selectedPetVaccinations = vaccinations .filter(vac => vac.petId === selectedPetId) - .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + .sort((a, b) => new Date(b.vaccination_date).getTime() - new Date(a.vaccination_date).getTime()); const formatDate = (dateString: string) => { const date = new Date(dateString); @@ -67,49 +93,36 @@ export default function VaccinationsScreen() { }); }; - const isOverdue = (nextDueDate?: string) => { - if (!nextDueDate) return false; - return new Date(nextDueDate) < new Date(); + const isOverdue = (expireDate?: string) => { + if (!expireDate) return false; + return new Date(expireDate) < new Date(); }; const renderVaccinationCard = (vaccination: Vaccination) => { - const overdue = isOverdue(vaccination.nextDueDate); + const overdue = isOverdue(vaccination.expire_date); return ( - {vaccination.name} + {vaccination.vac_name} - - {overdue ? 'Myöhässä' : vaccination.isUpToDate ? 'Voimassa' : 'Odottaa'} - + {vaccination.costs && ( + + {vaccination.costs} € + + )} - Annettu: {formatDate(vaccination.date)} + Annettu: {formatDate(vaccination.vaccination_date)} - {vaccination.nextDueDate && ( + {vaccination.expire_date && ( - Voimassa: {formatDate(vaccination.nextDueDate)} + Voimassa: {formatDate(vaccination.expire_date)} )} - - - - - - {vaccination.clinic} - - - - - - - {vaccination.veterinarian} - - - {vaccination.notes && ( <> From 314cb4ccbfc1adea01163fd66eee4af93045a7ed Mon Sep 17 00:00:00 2001 From: Samu Date: Tue, 27 Jan 2026 16:37:23 +0200 Subject: [PATCH 5/8] weights are displayed correctly now --- mobile/src/screens/WeightManagementScreen.tsx | 75 ++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx index 5a3913a..a4d52a9 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -8,33 +8,38 @@ import apiClient from '../services/api'; import { Pet } from '../types'; interface WeightRecord { - id: string; - petId: string; + id: number; + petId: number; date: string; weight: number; - unit: 'kg' | 'g'; + created_at: string; notes?: string; measuredBy?: string; } export default function WeightManagementScreen() { const [pets, setPets] = useState([]); - const [weightRecords] = useState([]); - const [selectedPetId, setSelectedPetId] = useState(''); + const [weightRecords, setWeightRecords] = useState([]); + const [selectedPetId, setSelectedPetId] = useState(null); const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Fetch pets from the API + // Fetch pets and weights from the API useEffect(() => { - const fetchPets = async () => { + const fetchData = async () => { try { setLoading(true); setError(null); - const response = await apiClient.get('/api/pets'); - if (response.data.success && response.data.data) { - const fetchedPets = response.data.data; + // Fetch both pets and weights in parallel + const [petsResponse, weightsResponse] = await Promise.all([ + apiClient.get('/api/pets'), + apiClient.get('/api/weights') + ]); + + if (petsResponse.data.success && petsResponse.data.data) { + const fetchedPets = petsResponse.data.data; setPets(fetchedPets); // Set the first pet as selected by default @@ -42,15 +47,44 @@ export default function WeightManagementScreen() { setSelectedPetId(fetchedPets[0].id); } } + + if (weightsResponse.data.success && weightsResponse.data.data) { + const weightsData = weightsResponse.data.data; + console.log('Weights response:', weightsData); + + // 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) => { + console.log('Raw weight data:', weight); + flattenedWeights.push({ + ...weight, + petId: petId, + weight: parseFloat(weight.weight) // Convert string to number + }); + }); + } + }); + console.log('Flattened weights:', flattenedWeights); + setWeightRecords(flattenedWeights); + } else { + // If flat structure + console.log('Flat weights data:', weightsData); + setWeightRecords(weightsData); + } + } } catch (err: any) { - console.error('Failed to fetch pets:', err); - setError('Lemmikit eivät latautuneet. Yritä uudelleen.'); + console.error('Failed to fetch data:', err); + setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { setLoading(false); } }; - fetchPets(); + fetchData(); }, []); const selectedPetWeights = weightRecords .filter(record => record.petId === selectedPetId) @@ -86,7 +120,17 @@ export default function WeightManagementScreen() { return { change: change.toFixed(1), percentChange, isIncrease: change > 0 }; }; + const formatWeight = (weight: number | undefined): string => { + if (weight === undefined || weight === null) { + console.log('Warning: weight is undefined or null'); + return '0'; + } + return weight % 1 === 0 ? weight.toFixed(0) : weight.toFixed(2).replace(/\.?0+$/, ''); + }; + const renderWeightCard = (record: WeightRecord, index: number) => { + console.log('Rendering weight record:', record); + const weightChange = calculateWeightChange(index); return ( @@ -95,10 +139,7 @@ export default function WeightManagementScreen() { - {record.weight} - - - {record.unit} + {formatWeight(record.weight)} kg {weightChange && ( From a639891d52008681c182c3c0c450fd5265797b7d Mon Sep 17 00:00:00 2001 From: Samu Date: Tue, 27 Jan 2026 16:42:21 +0200 Subject: [PATCH 6/8] fixed eruo icon --- mobile/src/screens/MedicationsScreen.tsx | 2 +- mobile/src/screens/VaccinationsScreen.tsx | 2 +- mobile/src/screens/VisitsScreen.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index e3de4b1..da85e6e 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -109,7 +109,7 @@ export default function MedicationsScreen() { {medication.med_name} {medication.costs && ( - + {medication.costs} € )} diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index cecd930..8eb028e 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -109,7 +109,7 @@ export default function VaccinationsScreen() { {vaccination.vac_name} {vaccination.costs && ( - + {vaccination.costs} € )} diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index 1457c3e..f7141e7 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -97,7 +97,7 @@ export default function VisitsScreen() { {visit.costs && ( - + {visit.costs} € )} From 5adab5aba1db88cf9ddc05a6bc71075cf5a4028d Mon Sep 17 00:00:00 2001 From: Samu Date: Wed, 28 Jan 2026 11:44:07 +0200 Subject: [PATCH 7/8] adding a visit form created & tested/datetime picker added --- mobile/package.json | 1 + mobile/src/screens/MedicationsScreen.tsx | 1 - mobile/src/screens/VisitsScreen.tsx | 339 +++++++++++++++++- mobile/src/screens/WeightManagementScreen.tsx | 7 - package-lock.json | 23 ++ 5 files changed, 358 insertions(+), 13 deletions(-) diff --git a/mobile/package.json b/mobile/package.json index c2d6001..baf1418 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/datetimepicker": "^8.6.0", "@react-navigation/bottom-tabs": "^7.10.0", "@react-navigation/native": "^7.1.0", "@react-navigation/native-stack": "^7.10.0", diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index da85e6e..ea1d5c2 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -49,7 +49,6 @@ export default function MedicationsScreen() { if (medicationsResponse.data.success && medicationsResponse.data.data) { // Check if medications are nested like visits const medicationsData = medicationsResponse.data.data; - console.log('Medications response:', medicationsData); // If nested structure (array of pet medication groups) if (Array.isArray(medicationsData) && medicationsData.length > 0 && medicationsData[0].medications) { diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index f7141e7..e83cb48 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -1,10 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import { View, ScrollView, StyleSheet } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator } from 'react-native-paper'; +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 { MaterialCommunityIcons } from '@expo/vector-icons'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; import { Pet } from '../types'; +import DateTimePicker from '@react-native-community/datetimepicker'; interface Visit { id: number; @@ -17,12 +18,55 @@ interface Visit { costs?: string; } +interface VisitType { + id: number; + name: string; +} + export default function VisitsScreen() { const [pets, setPets] = useState([]); const [visits, setVisits] = useState([]); + const [visitTypes, setVisitTypes] = useState([]); const [selectedPetId, setSelectedPetId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [saving, setSaving] = useState(false); + const [showDatePicker, setShowDatePicker] = useState(false); + const [keyboardHeight, setKeyboardHeight] = useState(0); + + 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]); + const [vetName, setVetName] = useState(''); + const [location, setLocation] = useState(''); + const [selectedTypeId, setSelectedTypeId] = useState(null); + const [notes, setNotes] = useState(''); + const [costs, setCosts] = useState(''); // Fetch pets and visits from the API useEffect(() => { @@ -31,7 +75,7 @@ export default function VisitsScreen() { setLoading(true); setError(null); - // Fetch both pets and visits in parallel + // Fetch pets and visits in parallel const [petsResponse, visitsResponse] = await Promise.all([ apiClient.get('/api/pets'), apiClient.get('/api/vet-visits') @@ -77,6 +121,85 @@ export default function VisitsScreen() { const selectedPetVisits = visits .filter(visit => visit.pet_id === selectedPetId) .sort((a, b) => new Date(b.visit_date).getTime() - new Date(a.visit_date).getTime()); + + const handleOpenModal = async () => { + // Reset form + setVisitDate(new Date().toISOString().split('T')[0]); + setVetName(''); + setLocation(''); + setSelectedTypeId(null); + setNotes(''); + setCosts(''); + setModalVisible(true); + + // 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); + } + } catch (err: any) { + console.error('Failed to fetch visit types:', err); + } + } + }; + + const handleCloseModal = () => { + setModalVisible(false); + }; + + const handleSaveVisit = async () => { + if (!selectedPetId || !vetName || !location || !selectedTypeId) { + alert('Täytä kaikki pakolliset kentät'); + return; + } + + 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); + } + + handleCloseModal(); + } + } catch (err: any) { + console.error('Failed to save visit:', err); + alert('Käynnin tallentaminen epäonnistui. Yritä uudelleen.'); + } finally { + setSaving(false); + } + }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -243,9 +366,185 @@ export default function VisitsScreen() { console.log('Lisää käynti')} + onPress={handleOpenModal} label="Lisää käynti" /> + + + 0 && { + marginTop: 10, + marginBottom: 10, + padding: SPACING.sm, + maxHeight: '45%' + } + ]} + > + + + Lisää käynti + + + 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 && ( + { + setShowDatePicker(false); + if (selectedDate) { + setVisitDate(selectedDate.toISOString().split('T')[0]); + } + }} + /> + )} + + + + + + + + Käynnin tyyppi * + + + {visitTypes.length > 0 ? ( + visitTypes.map((type) => ( + + )) + ) : ( + + )} + + + { + setTimeout(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, 200); + }} + /> + + { + setTimeout(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, 200); + }} + /> + + + + + + + + ); } @@ -368,4 +667,34 @@ const styles = StyleSheet.create({ 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 a4d52a9..ff72723 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -50,7 +50,6 @@ export default function WeightManagementScreen() { if (weightsResponse.data.success && weightsResponse.data.data) { const weightsData = weightsResponse.data.data; - console.log('Weights response:', weightsData); // If nested structure (array of pet weight groups) if (Array.isArray(weightsData) && weightsData.length > 0 && weightsData[0].weights) { @@ -59,7 +58,6 @@ export default function WeightManagementScreen() { const petId = petWeightGroup.pet_id; if (petWeightGroup.weights && Array.isArray(petWeightGroup.weights)) { petWeightGroup.weights.forEach((weight: any) => { - console.log('Raw weight data:', weight); flattenedWeights.push({ ...weight, petId: petId, @@ -68,11 +66,9 @@ export default function WeightManagementScreen() { }); } }); - console.log('Flattened weights:', flattenedWeights); setWeightRecords(flattenedWeights); } else { // If flat structure - console.log('Flat weights data:', weightsData); setWeightRecords(weightsData); } } @@ -122,15 +118,12 @@ export default function WeightManagementScreen() { const formatWeight = (weight: number | undefined): string => { if (weight === undefined || weight === null) { - console.log('Warning: weight is undefined or null'); return '0'; } return weight % 1 === 0 ? weight.toFixed(0) : weight.toFixed(2).replace(/\.?0+$/, ''); }; const renderWeightCard = (record: WeightRecord, index: number) => { - console.log('Rendering weight record:', record); - const weightChange = calculateWeightChange(index); return ( diff --git a/package-lock.json b/package-lock.json index 0a810e7..7742d39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "version": "1.0.0", "dependencies": { "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/datetimepicker": "^8.6.0", "@react-navigation/bottom-tabs": "^7.10.0", "@react-navigation/native": "^7.1.0", "@react-navigation/native-stack": "^7.10.0", @@ -4517,6 +4518,28 @@ "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, + "node_modules/@react-native-community/datetimepicker": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.6.0.tgz", + "integrity": "sha512-yxPSqNfxgpGaqHQIpatqe6ykeBdU/1pdsk/G3x01mY2bpTflLpmVTLqFSJYd3MiZzxNZcMs/j1dQakUczSjcYA==", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": ">=52.0.0", + "react": "*", + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", From f22e979cb7f36fd60fbabc6b58709fe120160fda Mon Sep 17 00:00:00 2001 From: Samu Date: Wed, 28 Jan 2026 14:39:09 +0200 Subject: [PATCH 8/8] forms to add, visits, medications, vaccinations and weights added. --- mobile/src/contexts/.gitkeep | 0 mobile/src/navigation/Navigation.tsx | 4 +- mobile/src/screens/MedicationsScreen.tsx | 312 +++++++++++++--- mobile/src/screens/VaccinationsScreen.tsx | 340 ++++++++++++++---- mobile/src/screens/VisitsScreen.tsx | 12 +- mobile/src/screens/WeightManagementScreen.tsx | 193 +++++++++- 6 files changed, 723 insertions(+), 138 deletions(-) delete mode 100644 mobile/src/contexts/.gitkeep diff --git a/mobile/src/contexts/.gitkeep b/mobile/src/contexts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/mobile/src/navigation/Navigation.tsx b/mobile/src/navigation/Navigation.tsx index a4dbe45..b00a5f0 100644 --- a/mobile/src/navigation/Navigation.tsx +++ b/mobile/src/navigation/Navigation.tsx @@ -18,8 +18,8 @@ import WalkDetailScreen from '@screens/WalkDetailScreen'; import HealthScreen from '@screens/HealthScreen'; import VisitsScreen from '@screens/VisitsScreen'; import MedicationsScreen from '@screens/MedicationsScreen'; -import VaccinationsScreen from '@screens/VaccinationsScreen'; -import WeightManagementScreen from '@screens/WeightManagementScreen'; +import VaccinationsScreen from '../screens/VaccinationsScreen'; +import WeightManagementScreen from '../screens/WeightManagementScreen'; const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index ea1d5c2..e88d657 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -1,14 +1,15 @@ -import React, { useState, useEffect } from 'react'; -import { View, ScrollView, StyleSheet } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator } from 'react-native-paper'; +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 { MaterialCommunityIcons } from '@expo/vector-icons'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; import { Pet } from '../types'; +import DateTimePicker from '@react-native-community/datetimepicker'; interface Medication { id: number; - petId: number; + pet_id: number; med_name: string; medication_date: string; expire_date?: string; @@ -22,6 +23,19 @@ export default function MedicationsScreen() { const [selectedPetId, setSelectedPetId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [saving, setSaving] = useState(false); + const [showMedicationDatePicker, setShowMedicationDatePicker] = useState(false); + const [showExpireDatePicker, setShowExpireDatePicker] = useState(false); + + const scrollViewRef = useRef(null); + + // Form state + const [medName, setMedName] = useState(''); + const [medicationDate, setMedicationDate] = useState(new Date().toISOString().split('T')[0]); + const [expireDate, setExpireDate] = useState(''); + const [costs, setCosts] = useState(''); + const [notes, setNotes] = useState(''); // Fetch pets and medications from the API useEffect(() => { @@ -30,7 +44,6 @@ export default function MedicationsScreen() { setLoading(true); setError(null); - // Fetch both pets and medications in parallel const [petsResponse, medicationsResponse] = await Promise.all([ apiClient.get('/api/pets'), apiClient.get('/api/medications') @@ -40,17 +53,14 @@ export default function MedicationsScreen() { const fetchedPets = petsResponse.data.data; setPets(fetchedPets); - // Set the first pet as selected by default if (fetchedPets.length > 0) { setSelectedPetId(fetchedPets[0].id); } } if (medicationsResponse.data.success && medicationsResponse.data.data) { - // Check if medications are nested like visits const medicationsData = medicationsResponse.data.data; - // If nested structure (array of pet medication groups) if (Array.isArray(medicationsData) && medicationsData.length > 0 && medicationsData[0].medications) { const flattenedMedications: Medication[] = []; medicationsData.forEach((petMedGroup: any) => { @@ -59,19 +69,17 @@ export default function MedicationsScreen() { petMedGroup.medications.forEach((med: any) => { flattenedMedications.push({ ...med, - petId: petId + pet_id: petId }); }); } }); setMedications(flattenedMedications); } else { - // If flat structure setMedications(medicationsData); } } } catch (err: any) { - console.error('Failed to fetch data:', err); setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { setLoading(false); @@ -82,11 +90,74 @@ export default function MedicationsScreen() { }, []); const selectedPetMedications = medications - .filter(med => med.petId === selectedPetId) - .sort((a, b) => { - // Sort by medication date, newest first - return new Date(b.medication_date).getTime() - new Date(a.medication_date).getTime(); - }); + .filter(med => med.pet_id === selectedPetId) + .sort((a, b) => new Date(b.medication_date).getTime() - new Date(a.medication_date).getTime()); + + const handleOpenModal = () => { + setMedName(''); + setMedicationDate(new Date().toISOString().split('T')[0]); + setExpireDate(''); + setCosts(''); + setNotes(''); + setModalVisible(true); + }; + + const handleCloseModal = () => { + setModalVisible(false); + }; + + const handleSaveMedication = async () => { + if (!selectedPetId || !medName) { + alert('Täytä kaikki pakolliset kentät'); + return; + } + + try { + setSaving(true); + + 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; + + 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(); + } + } catch (err: any) { + alert('Lääkityksen tallentaminen epäonnistui. Yritä uudelleen.'); + } finally { + setSaving(false); + } + }; const formatDate = (dateString: string) => { const date = new Date(dateString); @@ -258,9 +329,155 @@ export default function MedicationsScreen() { console.log('Lisää lääkitys')} + onPress={handleOpenModal} label="Lisää lääkitys" /> + + + + + + Lisää lääkitys + + + + + setShowMedicationDatePicker(true)}> + } + placeholder="PP-KK-VVVV" + placeholderTextColor="rgba(0, 0, 0, 0.3)" + textColor={COLORS.onSurface} + theme={{ colors: { onSurfaceVariant: 'rgba(0, 0, 0, 0.4)' } }} + /> + + + {showMedicationDatePicker && ( + { + setShowMedicationDatePicker(false); + if (selectedDate) { + setMedicationDate(selectedDate.toISOString().split('T')[0]); + } + }} + /> + )} + + setShowExpireDatePicker(true)}> + } + placeholder="PP-KK-VVVV (valinnainen)" + placeholderTextColor="rgba(0, 0, 0, 0.3)" + textColor={COLORS.onSurface} + theme={{ colors: { onSurfaceVariant: 'rgba(0, 0, 0, 0.4)' } }} + /> + + + {showExpireDatePicker && ( + { + setShowExpireDatePicker(false); + if (selectedDate) { + setExpireDate(selectedDate.toISOString().split('T')[0]); + } + }} + /> + )} + + { + setTimeout(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, 200); + }} + /> + + { + setTimeout(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, 200); + }} + /> + + + + + + + + ); } @@ -270,15 +487,6 @@ const styles = StyleSheet.create({ 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, @@ -335,21 +543,6 @@ const styles = StyleSheet.create({ color: COLORS.primary, flex: 1, }, - statusChip: { - marginLeft: SPACING.sm, - }, - activeChip: { - backgroundColor: '#E8F5E9', - }, - inactiveChip: { - backgroundColor: COLORS.surfaceVariant, - }, - activeChipText: { - color: '#2E7D32', - }, - inactiveChipText: { - color: COLORS.onSurfaceVariant, - }, dosageContainer: { flexDirection: 'row', alignItems: 'center', @@ -363,16 +556,6 @@ const styles = StyleSheet.create({ divider: { marginVertical: SPACING.sm, }, - medicationDetail: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.sm, - marginBottom: SPACING.xs, - }, - detailText: { - flex: 1, - color: COLORS.onSurface, - }, notesContainer: { flexDirection: 'row', gap: SPACING.sm, @@ -404,4 +587,31 @@ const styles = StyleSheet.create({ 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/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index 8eb028e..b6ce6d5 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -1,14 +1,15 @@ -import React, { useState, useEffect } from 'react'; -import { View, ScrollView, StyleSheet } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator } from 'react-native-paper'; +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 { MaterialCommunityIcons } from '@expo/vector-icons'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; import { Pet } from '../types'; +import DateTimePicker from '@react-native-community/datetimepicker'; interface Vaccination { id: number; - petId: number; + pet_id: number; vac_name: string; vaccination_date: string; expire_date?: string; @@ -22,6 +23,19 @@ export default function VaccinationsScreen() { const [selectedPetId, setSelectedPetId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [saving, setSaving] = useState(false); + const [showVaccinationDatePicker, setShowVaccinationDatePicker] = useState(false); + const [showExpireDatePicker, setShowExpireDatePicker] = useState(false); + + const scrollViewRef = useRef(null); + + // Form state + const [vacName, setVacName] = useState(''); + const [vaccinationDate, setVaccinationDate] = useState(new Date().toISOString().split('T')[0]); + const [expireDate, setExpireDate] = useState(''); + const [costs, setCosts] = useState(''); + const [notes, setNotes] = useState(''); // Fetch pets and vaccinations from the API useEffect(() => { @@ -30,7 +44,6 @@ export default function VaccinationsScreen() { setLoading(true); setError(null); - // Fetch both pets and vaccinations in parallel const [petsResponse, vaccinationsResponse] = await Promise.all([ apiClient.get('/api/pets'), apiClient.get('/api/vaccinations') @@ -40,7 +53,6 @@ export default function VaccinationsScreen() { const fetchedPets = petsResponse.data.data; setPets(fetchedPets); - // Set the first pet as selected by default if (fetchedPets.length > 0) { setSelectedPetId(fetchedPets[0].id); } @@ -49,7 +61,6 @@ export default function VaccinationsScreen() { if (vaccinationsResponse.data.success && vaccinationsResponse.data.data) { const vaccinationsData = vaccinationsResponse.data.data; - // If nested structure (array of pet vaccination groups) if (Array.isArray(vaccinationsData) && vaccinationsData.length > 0 && vaccinationsData[0].vaccinations) { const flattenedVaccinations: Vaccination[] = []; vaccinationsData.forEach((petVacGroup: any) => { @@ -58,19 +69,17 @@ export default function VaccinationsScreen() { petVacGroup.vaccinations.forEach((vac: any) => { flattenedVaccinations.push({ ...vac, - petId: petId + pet_id: petId }); }); } }); setVaccinations(flattenedVaccinations); } else { - // If flat structure setVaccinations(vaccinationsData); } } } catch (err: any) { - console.error('Failed to fetch data:', err); setError('Tietojen lataus epäonnistui. Yritä uudelleen.'); } finally { setLoading(false); @@ -81,9 +90,75 @@ export default function VaccinationsScreen() { }, []); const selectedPetVaccinations = vaccinations - .filter(vac => vac.petId === selectedPetId) + .filter(vac => vac.pet_id === selectedPetId) .sort((a, b) => new Date(b.vaccination_date).getTime() - new Date(a.vaccination_date).getTime()); + const handleOpenModal = () => { + setVacName(''); + setVaccinationDate(new Date().toISOString().split('T')[0]); + setExpireDate(''); + setCosts(''); + setNotes(''); + setModalVisible(true); + }; + + const handleCloseModal = () => { + setModalVisible(false); + }; + + const handleSaveVaccination = async () => { + if (!selectedPetId || !vacName) { + alert('Täytä kaikki pakolliset kentät'); + return; + } + + try { + setSaving(true); + + 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; + + 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(); + } + } catch (err: any) { + alert('Rokotuksen tallentaminen epäonnistui. Yritä uudelleen.'); + } finally { + setSaving(false); + } + }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -93,13 +168,8 @@ export default function VaccinationsScreen() { }); }; - const isOverdue = (expireDate?: string) => { - if (!expireDate) return false; - return new Date(expireDate) < new Date(); - }; - const renderVaccinationCard = (vaccination: Vaccination) => { - const overdue = isOverdue(vaccination.expire_date); + const isExpired = vaccination.expire_date ? new Date(vaccination.expire_date) < new Date() : false; return ( @@ -118,22 +188,25 @@ export default function VaccinationsScreen() { - Annettu: {formatDate(vaccination.vaccination_date)} + Rokotettu: {formatDate(vaccination.vaccination_date)} {vaccination.expire_date && ( - Voimassa: {formatDate(vaccination.expire_date)} + {isExpired ? 'Vanhentunut' : 'Uusittava'}: {formatDate(vaccination.expire_date)} )} @@ -256,9 +329,155 @@ export default function VaccinationsScreen() { console.log('Lisää rokotus')} + onPress={handleOpenModal} label="Lisää rokotus" /> + + + + + + Lisää rokotus + + + + + setShowVaccinationDatePicker(true)}> + } + placeholder="PP-KK-VVVV" + placeholderTextColor="rgba(0, 0, 0, 0.3)" + textColor={COLORS.onSurface} + theme={{ colors: { onSurfaceVariant: 'rgba(0, 0, 0, 0.4)' } }} + /> + + + {showVaccinationDatePicker && ( + { + setShowVaccinationDatePicker(false); + if (selectedDate) { + setVaccinationDate(selectedDate.toISOString().split('T')[0]); + } + }} + /> + )} + + setShowExpireDatePicker(true)}> + } + placeholder="PP-KK-VVVV (valinnainen)" + placeholderTextColor="rgba(0, 0, 0, 0.3)" + textColor={COLORS.onSurface} + theme={{ colors: { onSurfaceVariant: 'rgba(0, 0, 0, 0.4)' } }} + /> + + + {showExpireDatePicker && ( + { + setShowExpireDatePicker(false); + if (selectedDate) { + setExpireDate(selectedDate.toISOString().split('T')[0]); + } + }} + /> + )} + + { + setTimeout(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, 200); + }} + /> + + { + setTimeout(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, 200); + }} + /> + + + + + + + + ); } @@ -268,15 +487,6 @@ const styles = StyleSheet.create({ 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, @@ -333,56 +543,19 @@ const styles = StyleSheet.create({ color: COLORS.primary, flex: 1, }, - statusChip: { - marginLeft: SPACING.sm, - }, - upToDateChip: { - backgroundColor: '#E8F5E9', - }, - pendingChip: { - backgroundColor: '#FFF8E1', - }, - overdueChip: { - backgroundColor: '#FFEBEE', - }, - upToDateChipText: { - color: '#2E7D32', - }, - pendingChipText: { - color: '#F57C00', - }, - overdueChipText: { - color: '#D32F2F', - }, dateContainer: { flexDirection: 'row', alignItems: 'center', gap: SPACING.xs, + marginBottom: SPACING.sm, }, dateText: { fontWeight: '600', color: COLORS.onSurface, }, - nextDueText: { - color: COLORS.onSurfaceVariant, - }, - overdueText: { - color: '#D32F2F', - fontWeight: '600', - }, divider: { marginVertical: SPACING.sm, }, - vaccinationDetail: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.sm, - marginBottom: SPACING.xs, - }, - detailText: { - flex: 1, - color: COLORS.onSurface, - }, notesContainer: { flexDirection: 'row', gap: SPACING.sm, @@ -414,4 +587,31 @@ const styles = StyleSheet.create({ 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 e83cb48..145a1d5 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -374,15 +374,7 @@ export default function VisitsScreen() { 0 && { - marginTop: 10, - marginBottom: 10, - padding: SPACING.sm, - maxHeight: '45%' - } - ]} + contentContainerStyle={styles.modalContainer} > (new Date().getFullYear()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [saving, setSaving] = useState(false); + const [showDatePicker, setShowDatePicker] = useState(false); + + const scrollViewRef = useRef(null); + + // Form state + const [weight, setWeight] = useState(''); + const [date, setDate] = useState(new Date().toISOString().split('T')[0]); // Fetch pets and weights from the API useEffect(() => { @@ -96,6 +106,67 @@ export default function WeightManagementScreen() { record => new Date(record.date).getFullYear() === selectedYear ); + const handleOpenModal = () => { + setWeight(''); + setDate(new Date().toISOString().split('T')[0]); + setModalVisible(true); + }; + + const handleCloseModal = () => { + setModalVisible(false); + }; + + const handleSaveWeight = async () => { + if (!selectedPetId || !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 (response.data.success) { + const weightsResponse = await apiClient.get('/api/weights'); + if (weightsResponse.data.success && weightsResponse.data.data) { + const weightsData = weightsResponse.data.data; + + 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(); + } + } catch (err: any) { + alert('Painon tallentaminen epäonnistui. Yritä uudelleen.'); + } finally { + setSaving(false); + } + }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -284,7 +355,6 @@ export default function WeightManagementScreen() { // Add 10% padding to top and bottom for better visibility const paddedMax = maxWeight + (weightRange * 0.1); const paddedMin = minWeight - (weightRange * 0.1); - const paddedRange = paddedMax - paddedMin; const svgHeight = 220; // Make graph slightly wider for better readability const screenWidth = Dimensions.get('window').width; @@ -559,9 +629,95 @@ export default function WeightManagementScreen() { console.log('Lisää painomittaus')} + onPress={handleOpenModal} label="Lisää mittaus" /> + + + + + + Lisää painomittaus + + + { + setTimeout(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, 200); + }} + /> + + 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 && ( + { + setShowDatePicker(false); + if (selectedDate) { + setDate(selectedDate.toISOString().split('T')[0]); + } + }} + /> + )} + + + + + + + + ); } @@ -831,4 +987,31 @@ const styles = StyleSheet.create({ 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, + }, });