From 8f9d2d6c7a477cc1c05e3c73986ba04704ba8899 Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 29 Jan 2026 16:00:56 +0200 Subject: [PATCH 1/8] added buttons to edit & delete visits, medications, vaccinations and weights. --- mobile/src/navigation/Navigation.tsx | 8 ++ mobile/src/screens/HealthScreen.tsx | 28 ++----- mobile/src/screens/HomeScreen.tsx | 2 +- mobile/src/screens/MapScreen.tsx | 4 +- mobile/src/screens/MedicationsScreen.tsx | 57 +++++++++++-- mobile/src/screens/VaccinationsScreen.tsx | 56 +++++++++++-- mobile/src/screens/VisitsScreen.tsx | 57 +++++++++++-- mobile/src/screens/WeightManagementScreen.tsx | 82 ++++++++++++------- mobile/src/styles/authStyles.ts | 1 + mobile/src/styles/theme.ts | 4 +- 10 files changed, 228 insertions(+), 71 deletions(-) diff --git a/mobile/src/navigation/Navigation.tsx b/mobile/src/navigation/Navigation.tsx index b00a5f0..e2cab43 100644 --- a/mobile/src/navigation/Navigation.tsx +++ b/mobile/src/navigation/Navigation.tsx @@ -220,6 +220,14 @@ export default function Navigation() { headerBackTitle: 'Takaisin', }} /> + ) : ( <> 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..9e7829b 100644 --- a/mobile/src/screens/HomeScreen.tsx +++ b/mobile/src/screens/HomeScreen.tsx @@ -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..4b00d49 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -6,6 +6,7 @@ import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; import { Pet } from '../types'; import DateTimePicker from '@react-native-community/datetimepicker'; +import CardActions from 'react-native-paper/lib/typescript/components/Card/CardActions'; interface Medication { id: number; @@ -159,6 +160,16 @@ export default function MedicationsScreen() { } }; + const handleEditMedication = (medication: Medication) => { + // TODO: Implement edit functionality + console.log('Edit medication:', medication.id); + }; + + const handleDeleteMedication = async (medication: Medication) => { + // TODO: Implement delete functionality + console.log('Delete medication:', medication.id); + } + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -211,17 +222,29 @@ export default function MedicationsScreen() { )} - {medication.notes && ( - <> - + + + + {medication.notes ? ( {medication.notes} - - )} + ) : ( + + )} + + + handleEditMedication(medication)} style={styles.actionButton}> + + + handleDeleteMedication(medication)} style={styles.actionButton}> + + + + ); @@ -556,15 +579,37 @@ const styles = StyleSheet.create({ divider: { marginVertical: SPACING.sm, }, + bottomSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: SPACING.sm, + }, notesContainer: { flexDirection: 'row', gap: SPACING.sm, - marginTop: SPACING.xs, + 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, diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index b6ce6d5..b3ce9af 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -159,6 +159,16 @@ export default function VaccinationsScreen() { } }; + const handleEditVaccination = (vaccination: Vaccination) => { + // TODO: Implement edit functionality + console.log('Edit vaccination:', vaccination.id); + }; + + const handleDeleteVaccination = async (vaccination: Vaccination) => { + // TODO: Implement delete functionality + console.log('Delete vaccination:', vaccination.id); + } + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -211,17 +221,29 @@ export default function VaccinationsScreen() { )} - {vaccination.notes && ( - <> - + + + + {vaccination.notes ? ( {vaccination.notes} - - )} + ) : ( + + )} + + + handleEditVaccination(vaccination)} style={styles.actionButton}> + + + handleDeleteVaccination(vaccination)} style={styles.actionButton}> + + + + ); @@ -556,16 +578,38 @@ const styles = StyleSheet.create({ divider: { marginVertical: SPACING.sm, }, + bottomSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: SPACING.sm, + }, notesContainer: { flexDirection: 'row', gap: SPACING.sm, - marginTop: SPACING.xs, + 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', diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index 145a1d5..e53202d 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -200,6 +200,7 @@ export default function VisitsScreen() { } }; + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -209,6 +210,16 @@ export default function VisitsScreen() { }); }; + const handleEditVisit = (visit: Visit) => { + // TODO: Implement edit functionality + console.log('Edit visit:', visit.id); + }; + + const handleDeleteVisit = async (visit: Visit) => { + // TODO: Implement delete functionality + console.log('Delete visit:', visit.id); + }; + const renderVisitCard = (visit: Visit) => ( @@ -249,17 +260,29 @@ export default function VisitsScreen() { - {visit.notes && ( - <> - + + + + {visit.notes ? ( {visit.notes} - - )} + ) : ( + + )} + + + handleEditVisit(visit)} style={styles.actionButton}> + + + handleDeleteVisit(visit)} style={styles.actionButton}> + + + + ); @@ -628,16 +651,38 @@ const styles = StyleSheet.create({ flex: 1, color: COLORS.onSurface, }, + bottomSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: SPACING.sm, + }, notesContainer: { flexDirection: 'row', gap: SPACING.sm, - marginTop: SPACING.xs, + 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', diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx index 6b46af9..63f52d7 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -167,6 +167,16 @@ export default function WeightManagementScreen() { } }; + const handleEditWeightRecord = (weightRecord: WeightRecord) => { + // TODO: Implement edit functionality + console.log('Edit weight record:', weightRecord.id); + }; + + const handleDeleteWeightRecord = async (weightRecord: WeightRecord) => { + // TODO: Implement delete functionality + console.log('Delete weight record:', weightRecord.id); + } + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fi-FI', { @@ -223,36 +233,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}> + + - - )} + ); @@ -933,7 +936,8 @@ const styles = StyleSheet.create({ }, dateContainer: { flexDirection: 'row', - alignItems: 'center', + flex: 1, + alignItems: 'flex-start', gap: SPACING.xs, marginTop: SPACING.xs, }, @@ -952,16 +956,38 @@ const styles = StyleSheet.create({ detailText: { flex: 1, color: COLORS.onSurface, + }, + bottomSection: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: SPACING.sm, }, notesContainer: { flexDirection: 'row', gap: SPACING.sm, - marginTop: SPACING.xs, + 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, 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/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, }, From 1f2750f4bcd5b131af24cdcecd88bd80967f6473 Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 29 Jan 2026 16:15:35 +0200 Subject: [PATCH 2/8] moved styles to screenStyles file to be uniform with rest of the architecture --- mobile/src/screens/MedicationsScreen.tsx | 159 +-- mobile/src/screens/VaccinationsScreen.tsx | 158 +-- mobile/src/screens/VisitsScreen.tsx | 174 +--- mobile/src/screens/WeightManagementScreen.tsx | 319 +----- mobile/src/styles/screenStyles.ts | 971 ++++++++++++++++++ 5 files changed, 979 insertions(+), 802 deletions(-) diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index 4b00d49..46c487a 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, StyleSheet, TouchableOpacity } from 'react-native'; +import { View, ScrollView, 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 { medicationsStyles as styles } from '../styles/screenStyles'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; import { Pet } from '../types'; import DateTimePicker from '@react-native-community/datetimepicker'; -import CardActions from 'react-native-paper/lib/typescript/components/Card/CardActions'; interface Medication { id: number; @@ -505,158 +505,3 @@ export default function MedicationsScreen() { ); } -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, - }, - 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', - 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/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index b3ce9af..ffc992f 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, StyleSheet, TouchableOpacity } from 'react-native'; +import { View, ScrollView, 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 { vaccinationsStyles as styles } from '../styles/screenStyles'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; import { Pet } from '../types'; @@ -504,158 +505,3 @@ export default function VaccinationsScreen() { ); } -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, - }, - 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', - 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 e53202d..b99eb42 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, StyleSheet, TouchableOpacity, Keyboard } from 'react-native'; +import { View, ScrollView, 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 { visitsStyles as styles } from '../styles/screenStyles'; import { COLORS, SPACING } from '../styles/theme'; import apiClient from '../services/api'; import { Pet } from '../types'; @@ -564,174 +565,3 @@ export default function VisitsScreen() { ); } -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, - }, - 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', - 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 63f52d7..555feef 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, StyleSheet, Dimensions, TouchableOpacity } from 'react-native'; +import { View, ScrollView, Dimensions, TouchableOpacity } from 'react-native'; import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, Button, TextInput } 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 { Pet } from '../types'; @@ -725,319 +726,3 @@ export default function WeightManagementScreen() { ); } -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', - 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', - 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/styles/screenStyles.ts b/mobile/src/styles/screenStyles.ts index 1915a9c..11030bf 100644 --- a/mobile/src/styles/screenStyles.ts +++ b/mobile/src/styles/screenStyles.ts @@ -237,3 +237,974 @@ export const profileStyles = StyleSheet.create({ borderRadius: COMMON_STYLES.button.borderRadius, }, }); + +// ============================================ +// 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', + 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, + }, +}); + +// ============================================ +// 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', + 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, + }, +}); + +// ============================================ +// 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', + 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, + }, +}); + +// ============================================ +// 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', + 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, + }, +}); \ No newline at end of file From 8589522db62a12f7c0c488de086ca2bc8863a65c Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 29 Jan 2026 16:40:36 +0200 Subject: [PATCH 3/8] =?UTF-8?q?Pets=20nappi=20home=20screeniss=C3=A4=20vie?= =?UTF-8?q?=20Pets=20sivulle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/src/navigation/Navigation.tsx | 8 ++++++++ mobile/src/screens/HomeScreen.tsx | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mobile/src/navigation/Navigation.tsx b/mobile/src/navigation/Navigation.tsx index e2cab43..07ca821 100644 --- a/mobile/src/navigation/Navigation.tsx +++ b/mobile/src/navigation/Navigation.tsx @@ -228,6 +228,14 @@ export default function Navigation() { headerBackTitle: 'Takaisin', }} /> + ) : ( <> diff --git a/mobile/src/screens/HomeScreen.tsx b/mobile/src/screens/HomeScreen.tsx index 9e7829b..d5afb14 100644 --- a/mobile/src/screens/HomeScreen.tsx +++ b/mobile/src/screens/HomeScreen.tsx @@ -52,7 +52,7 @@ export default function HomeScreen() { - console.log('Lemmikki')}> + navigation.navigate('Pets' as never)}> Lemmikki From 2030183cab256cf058e00452dc55240b6ae16431 Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 29 Jan 2026 17:31:01 +0200 Subject: [PATCH 4/8] Service files created for all visits, medications, weights, vaccinations to handle api calls from the server --- mobile/src/screens/MedicationsScreen.tsx | 58 ++-------- mobile/src/screens/VaccinationsScreen.tsx | 56 ++------- mobile/src/screens/VisitsScreen.tsx | 52 ++------- mobile/src/screens/WeightManagementScreen.tsx | 60 ++-------- mobile/src/services/medicationsService.ts | 87 ++++++++++++++ mobile/src/services/vaccinationsService.ts | 88 ++++++++++++++ mobile/src/services/visitsService.ts | 107 ++++++++++++++++++ mobile/src/services/weightsService.ts | 94 +++++++++++++++ 8 files changed, 415 insertions(+), 187 deletions(-) create mode 100644 mobile/src/services/medicationsService.ts create mode 100644 mobile/src/services/vaccinationsService.ts create mode 100644 mobile/src/services/visitsService.ts create mode 100644 mobile/src/services/weightsService.ts diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index 46c487a..35073e9 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -5,6 +5,7 @@ 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'; @@ -17,7 +18,7 @@ interface Medication { costs?: string; notes?: string; } - + export default function MedicationsScreen() { const [pets, setPets] = useState([]); const [medications, setMedications] = useState([]); @@ -45,9 +46,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) { @@ -59,27 +60,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 { @@ -125,31 +106,12 @@ export default function MedicationsScreen() { notes: notes || undefined }; - const response = await apiClient.post('/api/medications', medicationData); + const newMedication = await medicationsService.createMedication(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); - } - } + if (newMedication) { + // Refresh medications + const refreshedMedications = await medicationsService.getAllMedications(); + setMedications(refreshedMedications); handleCloseModal(); } diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index ffc992f..a57e538 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -5,6 +5,7 @@ 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'; @@ -45,9 +46,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) { @@ -59,27 +60,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 { @@ -125,31 +106,12 @@ export default function VaccinationsScreen() { notes: notes || undefined }; - const response = await apiClient.post('/api/vaccinations', vaccinationData); + const newVaccination = await vaccinationsService.createVaccination(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); - } - } + if (newVaccination) { + // Refresh vaccinations + const refreshedVaccinations = await vaccinationsService.getAllVaccinations(); + setVaccinations(refreshedVaccinations); handleCloseModal(); } diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index b99eb42..889e458 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -5,6 +5,7 @@ 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'; @@ -77,9 +78,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) { @@ -92,22 +93,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.'); @@ -136,11 +122,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); } @@ -170,26 +154,12 @@ export default function VisitsScreen() { costs: costs ? parseFloat(costs) : undefined }; - const response = await apiClient.post('/api/vet-visits', visitData); + const newVisit = await visitsService.createVisit(visitData); - if (response.data.success) { + if (newVisit) { // 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); - } + const refreshedVisits = await visitsService.getAllVisits(); + setVisits(refreshedVisits); handleCloseModal(); } diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx index 555feef..7ef10f7 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -7,6 +7,7 @@ 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 { @@ -44,9 +45,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) { @@ -59,30 +60,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.'); @@ -132,32 +110,12 @@ export default function WeightManagementScreen() { date: date }; - const response = await apiClient.post('/api/weights', weightData); + const newWeight = await weightsService.createWeight(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); - } - } + if (newWeight) { + // Refresh weights + const refreshedWeights = await weightsService.getAllWeights(); + setWeightRecords(refreshedWeights); handleCloseModal(); } diff --git a/mobile/src/services/medicationsService.ts b/mobile/src/services/medicationsService.ts new file mode 100644 index 0000000..55f8bd8 --- /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?: number; +} + +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; + }, +}; From a8d79d7b80ffae663216804d780c745e2c80ec00 Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 29 Jan 2026 18:19:29 +0200 Subject: [PATCH 5/8] deletion and edit functionalities implemented to visit, medication, vaccination & weight cards. --- mobile/src/screens/MedicationsScreen.tsx | 107 ++++++++++++---- mobile/src/screens/VaccinationsScreen.tsx | 105 +++++++++++---- mobile/src/screens/VisitsScreen.tsx | 120 ++++++++++++++---- mobile/src/screens/WeightManagementScreen.tsx | 92 +++++++++++--- mobile/src/services/medicationsService.ts | 6 +- 5 files changed, 332 insertions(+), 98 deletions(-) diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index 35073e9..73bfd51 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -9,6 +9,7 @@ import { medicationsService } from '../services/medicationsService'; import { Pet } from '../types'; import DateTimePicker from '@react-native-community/datetimepicker'; + interface Medication { id: number; pet_id: number; @@ -29,6 +30,8 @@ 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 scrollViewRef = useRef(null); @@ -76,6 +79,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(''); @@ -86,34 +91,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 newMedication = await medicationsService.createMedication(medicationData); - - if (newMedication) { - // Refresh medications - const refreshedMedications = await medicationsService.getAllMedications(); - setMedications(refreshedMedications); - - handleCloseModal(); + 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); + + handleCloseModal(); + } } } catch (err: any) { alert('Lääkityksen tallentaminen epäonnistui. Yritä uudelleen.'); @@ -122,15 +155,39 @@ export default function MedicationsScreen() { } }; - const handleEditMedication = (medication: Medication) => { - // TODO: Implement edit functionality - console.log('Edit medication:', medication.id); + 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 = async (medication: Medication) => { - // TODO: Implement delete functionality - console.log('Delete medication:', medication.id); - } + if (window.confirm('Haluatko varmasti poistaa tämän lääkityksen?')) { + try { + const success = await medicationsService.deleteMedication(medication.id); + + if (success) { + // Refresh medications + const refreshedMedications = await medicationsService.getAllMedications(); + setMedications(refreshedMedications); + } 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); @@ -331,7 +388,7 @@ export default function MedicationsScreen() { contentContainerStyle={styles.scrollContentContainer} > - Lisää lääkitys + {isEditMode ? 'Muokkaa lääkitystä' : 'Lisää lääkitys'} (false); const [showVaccinationDatePicker, setShowVaccinationDatePicker] = useState(false); const [showExpireDatePicker, setShowExpireDatePicker] = useState(false); + const [editingVaccinationId, setEditingVaccinationId] = useState(null); + const [isEditMode, setIsEditMode] = useState(false); const scrollViewRef = useRef(null); @@ -76,6 +78,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(''); @@ -86,34 +91,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 newVaccination = await vaccinationsService.createVaccination(vaccinationData); - - if (newVaccination) { - // Refresh vaccinations - const refreshedVaccinations = await vaccinationsService.getAllVaccinations(); - setVaccinations(refreshedVaccinations); - - handleCloseModal(); + 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); + + 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(); + } } } catch (err: any) { alert('Rokotuksen tallentaminen epäonnistui. Yritä uudelleen.'); @@ -123,14 +157,37 @@ export default function VaccinationsScreen() { }; const handleEditVaccination = (vaccination: Vaccination) => { - // TODO: Implement edit functionality - console.log('Edit vaccination:', vaccination.id); + 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) => { - // TODO: Implement delete functionality - console.log('Delete vaccination:', vaccination.id); - } + if (confirm('Haluatko varmasti poistaa tämän rokotuksen?')) { + try { + const response = await vaccinationsService.deleteVaccination(vaccination.id); + + if (response) { + // Refresh vaccinations + const refreshedVaccinations = await vaccinationsService.getAllVaccinations(); + setVaccinations(refreshedVaccinations); + } 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); @@ -331,7 +388,7 @@ export default function VaccinationsScreen() { contentContainerStyle={styles.scrollContentContainer} > - Lisää rokotus + {isEditMode ? 'Muokkaa rokotusta' : 'Lisää rokotus'} (false); const [showDatePicker, setShowDatePicker] = useState(false); const [keyboardHeight, setKeyboardHeight] = useState(0); + const [editingVisitId, setEditingVisitId] = useState(null); + const [isEditMode, setIsEditMode] = useState(false); const scrollViewRef = useRef(null); const costsInputRef = useRef(null); @@ -110,7 +112,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(''); @@ -133,10 +137,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; } @@ -144,24 +150,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 newVisit = await visitsService.createVisit(visitData); - - if (newVisit) { - // Refresh visits - const refreshedVisits = await visitsService.getAllVisits(); - setVisits(refreshedVisits); + if (isEditMode && editingVisitId) { - handleCloseModal(); + 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; + } + + 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); @@ -181,14 +213,52 @@ export default function VisitsScreen() { }); }; - const handleEditVisit = (visit: Visit) => { - // TODO: Implement edit functionality - console.log('Edit visit:', visit.id); + const handleEditVisit = async (visit: Visit) => { + // Set edit mode + setIsEditMode(true); + setEditingVisitId(visit.id); + + // 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); + setSelectedTypeId(parseInt(visit.type_id)); + setNotes(visit.notes || ''); + setCosts(visit.costs || ''); + + // Fetch visit types if not already loaded + if (visitTypes.length === 0) { + try { + const types = await visitsService.getVisitTypes(); + setVisitTypes(types); + } catch (err: any) { + console.error('Failed to fetch visit types:', err); + } + } + + setModalVisible(true); }; const handleDeleteVisit = async (visit: Visit) => { - // TODO: Implement delete functionality - console.log('Delete visit:', visit.id); + // Show confirmation dialog + if (window.confirm(`Haluatko varmasti poistaa käynnin ${formatDate(visit.visit_date)}?`)) { + try { + const success = await visitsService.deleteVisit(visit.id); + + if (success) { + // Refresh visits + const refreshedVisits = await visitsService.getAllVisits(); + setVisits(refreshedVisits); + } 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) => ( @@ -377,7 +447,7 @@ export default function VisitsScreen() { contentContainerStyle={styles.scrollContentContainer} > - Lisää käynti + {isEditMode ? 'Muokkaa käyntiä' : 'Lisää käynti'} setShowDatePicker(true)}> diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx index 7ef10f7..725f108 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -30,6 +30,8 @@ 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 scrollViewRef = useRef(null); @@ -86,6 +88,8 @@ export default function WeightManagementScreen() { ); const handleOpenModal = () => { + setIsEditMode(false); + setEditingWeightId(null); setWeight(''); setDate(new Date().toISOString().split('T')[0]); setModalVisible(true); @@ -93,31 +97,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 newWeight = await weightsService.createWeight(weightData); + if (isEditMode && editingWeightId) { - if (newWeight) { - // Refresh weights - const refreshedWeights = await weightsService.getAllWeights(); - setWeightRecords(refreshedWeights); - - handleCloseModal(); + 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); + + handleCloseModal(); + } } } catch (err: any) { alert('Painon tallentaminen epäonnistui. Yritä uudelleen.'); @@ -126,15 +156,35 @@ export default function WeightManagementScreen() { } }; - const handleEditWeightRecord = (weightRecord: WeightRecord) => { - // TODO: Implement edit functionality - console.log('Edit weight record:', weightRecord.id); + 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) => { - // TODO: Implement delete functionality - console.log('Delete weight record:', weightRecord.id); - } + if (!confirm('Haluatko varmasti poistaa tämän painomittauksen?')) { + try { + const response = await weightsService.deleteWeight(weightRecord.id); + + if (response) { + // Refresh weights + const refreshedWeights = await weightsService.getAllWeights(); + setWeightRecords(refreshedWeights); + } 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); @@ -608,7 +658,7 @@ export default function WeightManagementScreen() { contentContainerStyle={styles.scrollContentContainer} > - Lisää painomittaus + {isEditMode ? 'Muokkaa painomittausta' : 'Lisää painomittaus'} Date: Thu, 29 Jan 2026 18:27:23 +0200 Subject: [PATCH 6/8] fix to automatically select the visit tyoe during edit --- mobile/src/screens/VisitsScreen.tsx | 32 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index 31851f9..cd8af1a 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -218,26 +218,36 @@ export default function VisitsScreen() { 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); - setSelectedTypeId(parseInt(visit.type_id)); - setNotes(visit.notes || ''); - setCosts(visit.costs || ''); - // Fetch visit types if not already loaded - if (visitTypes.length === 0) { - try { - const types = await visitsService.getVisitTypes(); - setVisitTypes(types); - } catch (err: any) { - console.error('Failed to fetch visit types:', err); - } + // 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) { + console.log('Found matching type:', matchingType); + setSelectedTypeId(matchingType.id); + } else { + console.log('No matching type found for:', visit.type_id); } + setNotes(visit.notes || ''); + setCosts(visit.costs || ''); + setModalVisible(true); }; From 090548058f46e429899722bc935645c246951d99 Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 29 Jan 2026 18:45:58 +0200 Subject: [PATCH 7/8] deletion & edit functions tested and fixed for all visits, medications, vaccinations and weights. --- mobile/src/screens/MedicationsScreen.tsx | 52 +++++++++++----- mobile/src/screens/VaccinationsScreen.tsx | 50 +++++++++++---- mobile/src/screens/VisitsScreen.tsx | 61 +++++++++++++------ mobile/src/screens/WeightManagementScreen.tsx | 48 +++++++++++---- 4 files changed, 151 insertions(+), 60 deletions(-) diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx index 73bfd51..30e40f5 100644 --- a/mobile/src/screens/MedicationsScreen.tsx +++ b/mobile/src/screens/MedicationsScreen.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { View, ScrollView, TouchableOpacity } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper'; +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'; @@ -32,6 +32,8 @@ export default function MedicationsScreen() { 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); @@ -170,22 +172,29 @@ export default function MedicationsScreen() { setModalVisible(true); }; - const handleDeleteMedication = async (medication: Medication) => { - if (window.confirm('Haluatko varmasti poistaa tämän lääkityksen?')) { - try { - const success = await medicationsService.deleteMedication(medication.id); + const handleDeleteMedication = (medication: Medication) => { + setMedicationToDelete(medication); + setDeleteDialogVisible(true); + }; - if (success) { - // Refresh medications - const refreshedMedications = await medicationsService.getAllMedications(); - setMedications(refreshedMedications); - } else { - alert('Lääkityksen poistaminen epäonnistui. Yritä uudelleen.'); - } - } catch (err: any) { - console.error("Failed to delete medication:", err); + 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.'); } }; @@ -519,6 +528,21 @@ export default function MedicationsScreen() { + setDeleteDialogVisible(false)} + > + Vahvista poisto + + + Haluatko varmasti poistaa tämän lääkityksen? + + + + + + + ); diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index b1ec262..74132cd 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { View, ScrollView, TouchableOpacity } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper'; +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'; @@ -31,6 +31,8 @@ export default function VaccinationsScreen() { 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); @@ -171,21 +173,28 @@ export default function VaccinationsScreen() { }; const handleDeleteVaccination = async (vaccination: Vaccination) => { - if (confirm('Haluatko varmasti poistaa tämän rokotuksen?')) { - try { - const response = await vaccinationsService.deleteVaccination(vaccination.id); + setVaccinationToDelete(vaccination); + setDeleteDialogVisible(true); + }; - if (response) { - // Refresh vaccinations - const refreshedVaccinations = await vaccinationsService.getAllVaccinations(); - setVaccinations(refreshedVaccinations); - } else { - alert('Rokotuksen poistaminen epäonnistui. Yritä uudelleen.'); - } - } catch (err: any) { - console.error("Failed to delete vaccination:", err); + 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.'); } }; @@ -519,6 +528,21 @@ export default function VaccinationsScreen() { + setDeleteDialogVisible(false)} + > + Vahvista poisto + + Haluatko varmasti poistaa tämän rokotuksen? + + + + + + ); diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx index cd8af1a..e4003c4 100644 --- a/mobile/src/screens/VisitsScreen.tsx +++ b/mobile/src/screens/VisitsScreen.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { View, ScrollView, TouchableOpacity, Keyboard } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper'; +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'; @@ -38,6 +38,8 @@ export default function VisitsScreen() { 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); @@ -239,10 +241,7 @@ export default function VisitsScreen() { // 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) { - console.log('Found matching type:', matchingType); setSelectedTypeId(matchingType.id); - } else { - console.log('No matching type found for:', visit.type_id); } setNotes(visit.notes || ''); @@ -251,23 +250,29 @@ export default function VisitsScreen() { setModalVisible(true); }; - const handleDeleteVisit = async (visit: Visit) => { - // Show confirmation dialog - if (window.confirm(`Haluatko varmasti poistaa käynnin ${formatDate(visit.visit_date)}?`)) { - try { - const success = await visitsService.deleteVisit(visit.id); - - if (success) { - // Refresh visits - const refreshedVisits = await visitsService.getAllVisits(); - setVisits(refreshedVisits); - } 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 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.'); } }; @@ -610,6 +615,22 @@ export default function VisitsScreen() { + + setDeleteDialogVisible(false)} + > + Poista käynti + + + {visitToDelete && `Haluatko varmasti poistaa käynnin ${formatDate(visitToDelete.visit_date)}?`} + + + + + + + ); diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx index 725f108..a358da3 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { View, ScrollView, Dimensions, TouchableOpacity } from 'react-native'; -import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, Button, TextInput } from 'react-native-paper'; +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'; @@ -32,6 +32,8 @@ export default function WeightManagementScreen() { 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); @@ -168,21 +170,28 @@ export default function WeightManagementScreen() { }; const handleDeleteWeightRecord = async (weightRecord: WeightRecord) => { - if (!confirm('Haluatko varmasti poistaa tämän painomittauksen?')) { - try { - const response = await weightsService.deleteWeight(weightRecord.id); + setWeightToDelete(weightRecord); + setDeleteDialogVisible(true); + }; - if (response) { - // Refresh weights - const refreshedWeights = await weightsService.getAllWeights(); - setWeightRecords(refreshedWeights); - } else { - alert('Painomittauksen poistaminen epäonnistui. Yritä uudelleen.'); - } - } catch (err: any) { - console.error("Failed to delete weight record:", err); + 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.'); } }; @@ -729,6 +738,19 @@ export default function WeightManagementScreen() { + setDeleteDialogVisible(false)} + > + Poista painomittaus + + Haluatko varmasti poistaa tämän painomittauksen? + + + + + + ); From d6ea4254aa9c28215b94aec43f6abf6d45896541 Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 29 Jan 2026 18:53:20 +0200 Subject: [PATCH 8/8] =?UTF-8?q?poiston=20varmistamisen=20dialogin=20yleisn?= =?UTF-8?q?=C3=A4k=C3=B6=20yht=C3=A4l=C3=A4iseksi=20kaikissa=20nelj=C3=A4s?= =?UTF-8?q?s=C3=A4=20osiossa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/src/screens/VaccinationsScreen.tsx | 4 +--- mobile/src/screens/WeightManagementScreen.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx index 74132cd..442459c 100644 --- a/mobile/src/screens/VaccinationsScreen.tsx +++ b/mobile/src/screens/VaccinationsScreen.tsx @@ -538,9 +538,7 @@ export default function VaccinationsScreen() { - + diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx index a358da3..0b731ca 100644 --- a/mobile/src/screens/WeightManagementScreen.tsx +++ b/mobile/src/screens/WeightManagementScreen.tsx @@ -748,7 +748,7 @@ export default function WeightManagementScreen() { - +