diff --git a/mobile/src/navigation/Navigation.tsx b/mobile/src/navigation/Navigation.tsx
index b00a5f0..07ca821 100644
--- a/mobile/src/navigation/Navigation.tsx
+++ b/mobile/src/navigation/Navigation.tsx
@@ -220,6 +220,22 @@ 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..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
@@ -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..30e40f5 100644
--- a/mobile/src/screens/MedicationsScreen.tsx
+++ b/mobile/src/screens/MedicationsScreen.tsx
@@ -1,12 +1,15 @@
import React, { useState, useEffect, useRef } from 'react';
-import { View, ScrollView, StyleSheet, TouchableOpacity } from 'react-native';
-import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper';
+import { View, ScrollView, TouchableOpacity } from 'react-native';
+import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button, Dialog } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { medicationsStyles as styles } from '../styles/screenStyles';
import { COLORS, SPACING } from '../styles/theme';
import apiClient from '../services/api';
+import { medicationsService } from '../services/medicationsService';
import { Pet } from '../types';
import DateTimePicker from '@react-native-community/datetimepicker';
+
interface Medication {
id: number;
pet_id: number;
@@ -16,7 +19,7 @@ interface Medication {
costs?: string;
notes?: string;
}
-
+
export default function MedicationsScreen() {
const [pets, setPets] = useState([]);
const [medications, setMedications] = useState([]);
@@ -27,6 +30,10 @@ export default function MedicationsScreen() {
const [saving, setSaving] = useState(false);
const [showMedicationDatePicker, setShowMedicationDatePicker] = useState(false);
const [showExpireDatePicker, setShowExpireDatePicker] = useState(false);
+ const [editingMedicationId, setEditingMedicationId] = useState(null);
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [deleteDialogVisible, setDeleteDialogVisible] = useState(false);
+ const [medicationToDelete, setMedicationToDelete] = useState(null);
const scrollViewRef = useRef(null);
@@ -44,9 +51,9 @@ export default function MedicationsScreen() {
setLoading(true);
setError(null);
- const [petsResponse, medicationsResponse] = await Promise.all([
+ const [petsResponse, fetchedMedications] = await Promise.all([
apiClient.get('/api/pets'),
- apiClient.get('/api/medications')
+ medicationsService.getAllMedications()
]);
if (petsResponse.data.success && petsResponse.data.data) {
@@ -58,27 +65,7 @@ export default function MedicationsScreen() {
}
}
- if (medicationsResponse.data.success && medicationsResponse.data.data) {
- const medicationsData = medicationsResponse.data.data;
-
- if (Array.isArray(medicationsData) && medicationsData.length > 0 && medicationsData[0].medications) {
- const flattenedMedications: Medication[] = [];
- medicationsData.forEach((petMedGroup: any) => {
- const petId = petMedGroup.pet_id;
- if (petMedGroup.medications && Array.isArray(petMedGroup.medications)) {
- petMedGroup.medications.forEach((med: any) => {
- flattenedMedications.push({
- ...med,
- pet_id: petId
- });
- });
- }
- });
- setMedications(flattenedMedications);
- } else {
- setMedications(medicationsData);
- }
- }
+ setMedications(fetchedMedications);
} catch (err: any) {
setError('Tietojen lataus epäonnistui. Yritä uudelleen.');
} finally {
@@ -94,6 +81,8 @@ export default function MedicationsScreen() {
.sort((a, b) => new Date(b.medication_date).getTime() - new Date(a.medication_date).getTime());
const handleOpenModal = () => {
+ setIsEditMode(false);
+ setEditingMedicationId(null);
setMedName('');
setMedicationDate(new Date().toISOString().split('T')[0]);
setExpireDate('');
@@ -104,53 +93,62 @@ export default function MedicationsScreen() {
const handleCloseModal = () => {
setModalVisible(false);
+ setIsEditMode(false);
+ setEditingMedicationId(null);
};
const handleSaveMedication = async () => {
- if (!selectedPetId || !medName) {
+ if (!medName) {
alert('Täytä kaikki pakolliset kentät');
return;
}
try {
setSaving(true);
+
+ if (isEditMode && editingMedicationId) {
+
+ const medicationData = {
+ med_name: medName,
+ medication_date: medicationDate,
+ expire_date: expireDate || undefined,
+ costs: costs ? parseFloat(costs) : undefined,
+ notes: notes || undefined
+ };
+
+ const updatedMedication = await medicationsService.updateMedication(editingMedicationId, medicationData);
+
+ if (updatedMedication) {
+ // Refresh medications
+ const refreshedMedications = await medicationsService.getAllMedications();
+ setMedications(refreshedMedications);
+
+ handleCloseModal();
+ }
+ } else {
+ if (!selectedPetId) {
+ alert('Valitse lemmikki ennen tallentamista.');
+ return;
+ }
- const medicationData = {
- pet_id: selectedPetId,
- med_name: medName,
- medication_date: medicationDate,
- expire_date: expireDate || undefined,
- costs: costs ? parseFloat(costs) : undefined,
- notes: notes || undefined
- };
-
- const response = await apiClient.post('/api/medications', medicationData);
-
- if (response.data.success) {
- const medicationsResponse = await apiClient.get('/api/medications');
- if (medicationsResponse.data.success && medicationsResponse.data.data) {
- const medicationsData = medicationsResponse.data.data;
+ const medicationData = {
+ pet_id: selectedPetId,
+ med_name: medName,
+ medication_date: medicationDate,
+ expire_date: expireDate || undefined,
+ costs: costs ? parseFloat(costs) : undefined,
+ notes: notes || undefined
+ };
+
+ const newMedication = await medicationsService.createMedication(medicationData);
+
+ if (newMedication) {
+ // Refresh medications
+ const refreshedMedications = await medicationsService.getAllMedications();
+ setMedications(refreshedMedications);
- if (Array.isArray(medicationsData) && medicationsData.length > 0 && medicationsData[0].medications) {
- const flattenedMedications: Medication[] = [];
- medicationsData.forEach((petMedGroup: any) => {
- const petId = petMedGroup.pet_id;
- if (petMedGroup.medications && Array.isArray(petMedGroup.medications)) {
- petMedGroup.medications.forEach((med: any) => {
- flattenedMedications.push({
- ...med,
- pet_id: petId
- });
- });
- }
- });
- setMedications(flattenedMedications);
- } else {
- setMedications(medicationsData);
- }
+ handleCloseModal();
}
-
- handleCloseModal();
}
} catch (err: any) {
alert('Lääkityksen tallentaminen epäonnistui. Yritä uudelleen.');
@@ -159,6 +157,47 @@ export default function MedicationsScreen() {
}
};
+ const handleEditMedication = (medication: Medication) => {
+
+ setIsEditMode(true);
+ setEditingMedicationId(medication.id);
+
+ const dateOnly = medication.medication_date.split('T')[0];
+ setMedName(medication.med_name);
+ setMedicationDate(dateOnly);
+ const expireDateOnly = medication.expire_date ? medication.expire_date.split('T')[0] : '';
+ setExpireDate(expireDateOnly);
+ setCosts(medication.costs || '');
+ setNotes(medication.notes || '');
+ setModalVisible(true);
+ };
+
+ const handleDeleteMedication = (medication: Medication) => {
+ setMedicationToDelete(medication);
+ setDeleteDialogVisible(true);
+ };
+
+ const confirmDeleteMedication = async () => {
+ if (!medicationToDelete) return;
+
+ try {
+ const success = await medicationsService.deleteMedication(medicationToDelete.id);
+
+ if (success) {
+ // Refresh medications
+ const refreshedMedications = await medicationsService.getAllMedications();
+ setMedications(refreshedMedications);
+ setDeleteDialogVisible(false);
+ setMedicationToDelete(null);
+ } else {
+ alert('Lääkityksen poistaminen epäonnistui. Yritä uudelleen.');
+ }
+ } catch (err: any) {
+ console.error("Failed to delete medication:", err);
+ alert('Lääkityksen poistaminen epäonnistui. Yritä uudelleen.');
+ }
+ };
+
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('fi-FI', {
@@ -211,17 +250,29 @@ export default function MedicationsScreen() {
)}
- {medication.notes && (
- <>
-
+
+
+
+ {medication.notes ? (
{medication.notes}
- >
- )}
+ ) : (
+
+ )}
+
+
+ handleEditMedication(medication)} style={styles.actionButton}>
+
+
+ handleDeleteMedication(medication)} style={styles.actionButton}>
+
+
+
+
);
@@ -346,7 +397,7 @@ export default function MedicationsScreen() {
contentContainerStyle={styles.scrollContentContainer}
>
- Lisää lääkitys
+ {isEditMode ? 'Muokkaa lääkitystä' : 'Lisää lääkitys'}
+
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: COLORS.background,
- },
- tabsContainer: {
- maxHeight: 70,
- backgroundColor: COLORS.surface,
- borderBottomWidth: 1,
- borderBottomColor: COLORS.surfaceVariant,
- },
- tabsContent: {
- paddingHorizontal: SPACING.md,
- paddingVertical: SPACING.md,
- gap: SPACING.sm,
- alignItems: 'center',
- justifyContent: 'center',
- flexGrow: 1,
- },
- tab: {
- marginHorizontal: SPACING.xs,
- paddingHorizontal: SPACING.md,
- height: 40,
- justifyContent: 'center',
- alignItems: 'center',
- },
- selectedTab: {
- backgroundColor: COLORS.primary,
- elevation: 3,
- },
- selectedTabText: {
- color: '#FFFFFF',
- fontWeight: '700',
- fontSize: 15,
- lineHeight: 20,
- },
- unselectedTabText: {
- fontSize: 15,
- lineHeight: 20,
- },
- content: {
- flex: 1,
- },
- scrollContent: {
- padding: SPACING.md,
- },
- medicationCard: {
- marginBottom: SPACING.md,
- elevation: 2,
- },
- medicationHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: SPACING.sm,
- },
- medicationName: {
- fontWeight: '700',
- color: COLORS.primary,
- flex: 1,
- },
- dosageContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING.xs,
- marginBottom: SPACING.sm,
- },
- dosage: {
- fontWeight: '600',
- color: COLORS.onSurface,
- },
- divider: {
- marginVertical: SPACING.sm,
- },
- notesContainer: {
- flexDirection: 'row',
- gap: SPACING.sm,
- marginTop: SPACING.xs,
- },
- notesText: {
- flex: 1,
- color: COLORS.onSurfaceVariant,
- fontStyle: 'italic',
- },
- emptyContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingVertical: SPACING.xl * 2,
- },
- emptyTitle: {
- marginTop: SPACING.md,
- color: COLORS.onSurfaceVariant,
- },
- emptyText: {
- marginTop: SPACING.xs,
- color: COLORS.onSurfaceVariant,
- textAlign: 'center',
- },
- fab: {
- position: 'absolute',
- right: SPACING.md,
- bottom: SPACING.md,
- backgroundColor: COLORS.primary,
- },
- modalContainer: {
- backgroundColor: COLORS.surface,
- margin: SPACING.lg,
- padding: SPACING.lg,
- borderRadius: 12,
- maxHeight: '90%',
- },
- scrollContentContainer: {
- paddingBottom: 0,
- },
- modalTitle: {
- marginBottom: SPACING.lg,
- fontWeight: 'bold',
- color: COLORS.onSurface,
- },
- input: {
- marginBottom: SPACING.md,
- },
- modalButtons: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginTop: SPACING.lg,
- gap: SPACING.md,
- },
- modalButton: {
- flex: 1,
- },
-});
diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx
index b6ce6d5..442459c 100644
--- a/mobile/src/screens/VaccinationsScreen.tsx
+++ b/mobile/src/screens/VaccinationsScreen.tsx
@@ -1,9 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
-import { View, ScrollView, StyleSheet, TouchableOpacity } from 'react-native';
-import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper';
+import { View, ScrollView, TouchableOpacity } from 'react-native';
+import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button, Dialog } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { vaccinationsStyles as styles } from '../styles/screenStyles';
import { COLORS, SPACING } from '../styles/theme';
import apiClient from '../services/api';
+import { vaccinationsService } from '../services/vaccinationsService';
import { Pet } from '../types';
import DateTimePicker from '@react-native-community/datetimepicker';
@@ -27,6 +29,10 @@ export default function VaccinationsScreen() {
const [saving, setSaving] = useState(false);
const [showVaccinationDatePicker, setShowVaccinationDatePicker] = useState(false);
const [showExpireDatePicker, setShowExpireDatePicker] = useState(false);
+ const [editingVaccinationId, setEditingVaccinationId] = useState(null);
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [deleteDialogVisible, setDeleteDialogVisible] = useState(false);
+ const [vaccinationToDelete, setVaccinationToDelete] = useState(null);
const scrollViewRef = useRef(null);
@@ -44,9 +50,9 @@ export default function VaccinationsScreen() {
setLoading(true);
setError(null);
- const [petsResponse, vaccinationsResponse] = await Promise.all([
+ const [petsResponse, fetchedVaccinations] = await Promise.all([
apiClient.get('/api/pets'),
- apiClient.get('/api/vaccinations')
+ vaccinationsService.getAllVaccinations()
]);
if (petsResponse.data.success && petsResponse.data.data) {
@@ -58,27 +64,7 @@ export default function VaccinationsScreen() {
}
}
- if (vaccinationsResponse.data.success && vaccinationsResponse.data.data) {
- const vaccinationsData = vaccinationsResponse.data.data;
-
- if (Array.isArray(vaccinationsData) && vaccinationsData.length > 0 && vaccinationsData[0].vaccinations) {
- const flattenedVaccinations: Vaccination[] = [];
- vaccinationsData.forEach((petVacGroup: any) => {
- const petId = petVacGroup.pet_id;
- if (petVacGroup.vaccinations && Array.isArray(petVacGroup.vaccinations)) {
- petVacGroup.vaccinations.forEach((vac: any) => {
- flattenedVaccinations.push({
- ...vac,
- pet_id: petId
- });
- });
- }
- });
- setVaccinations(flattenedVaccinations);
- } else {
- setVaccinations(vaccinationsData);
- }
- }
+ setVaccinations(fetchedVaccinations);
} catch (err: any) {
setError('Tietojen lataus epäonnistui. Yritä uudelleen.');
} finally {
@@ -94,6 +80,9 @@ export default function VaccinationsScreen() {
.sort((a, b) => new Date(b.vaccination_date).getTime() - new Date(a.vaccination_date).getTime());
const handleOpenModal = () => {
+ // Reset form for create mode
+ setIsEditMode(false);
+ setEditingVaccinationId(null);
setVacName('');
setVaccinationDate(new Date().toISOString().split('T')[0]);
setExpireDate('');
@@ -104,53 +93,63 @@ export default function VaccinationsScreen() {
const handleCloseModal = () => {
setModalVisible(false);
+ setIsEditMode(false);
+ setEditingVaccinationId(null);
};
const handleSaveVaccination = async () => {
- if (!selectedPetId || !vacName) {
+ if (!vacName) {
alert('Täytä kaikki pakolliset kentät');
return;
}
try {
setSaving(true);
+
+ if (isEditMode && editingVaccinationId) {
- const vaccinationData = {
- pet_id: selectedPetId,
- vac_name: vacName,
- vaccination_date: vaccinationDate,
- expire_date: expireDate || undefined,
- costs: costs ? parseFloat(costs) : undefined,
- notes: notes || undefined
- };
-
- const response = await apiClient.post('/api/vaccinations', vaccinationData);
-
- if (response.data.success) {
- const vaccinationsResponse = await apiClient.get('/api/vaccinations');
- if (vaccinationsResponse.data.success && vaccinationsResponse.data.data) {
- const vaccinationsData = vaccinationsResponse.data.data;
+ const vaccinationData = {
+ pet_id: selectedPetId,
+ vac_name: vacName,
+ vaccination_date: vaccinationDate,
+ expire_date: expireDate || undefined,
+ costs: costs ? parseFloat(costs) : undefined,
+ notes: notes || undefined
+ };
+
+ const updatedVaccination = await vaccinationsService.updateVaccination(editingVaccinationId, vaccinationData);
+
+ if (updatedVaccination) {
+ // Refresh vaccinations
+ const refreshedVaccinations = await vaccinationsService.getAllVaccinations();
+ setVaccinations(refreshedVaccinations);
- if (Array.isArray(vaccinationsData) && vaccinationsData.length > 0 && vaccinationsData[0].vaccinations) {
- const flattenedVaccinations: Vaccination[] = [];
- vaccinationsData.forEach((petVacGroup: any) => {
- const petId = petVacGroup.pet_id;
- if (petVacGroup.vaccinations && Array.isArray(petVacGroup.vaccinations)) {
- petVacGroup.vaccinations.forEach((vac: any) => {
- flattenedVaccinations.push({
- ...vac,
- pet_id: petId
- });
- });
- }
- });
- setVaccinations(flattenedVaccinations);
- } else {
- setVaccinations(vaccinationsData);
- }
+ handleCloseModal();
+ }
+ } else {
+ if (!selectedPetId) {
+ alert('Valitse lemmikki ennen tallentamista.');
+ return;
+ }
+
+ const vaccinationData = {
+ pet_id: selectedPetId,
+ vac_name: vacName,
+ vaccination_date: vaccinationDate,
+ expire_date: expireDate || undefined,
+ costs: costs ? parseFloat(costs) : undefined,
+ notes: notes || undefined
+ };
+
+ const newVaccination = await vaccinationsService.createVaccination(vaccinationData);
+
+ if (newVaccination) {
+ // Refresh vaccinations
+ const refreshedVaccinations = await vaccinationsService.getAllVaccinations();
+ setVaccinations(refreshedVaccinations);
+
+ handleCloseModal();
}
-
- handleCloseModal();
}
} catch (err: any) {
alert('Rokotuksen tallentaminen epäonnistui. Yritä uudelleen.');
@@ -159,6 +158,46 @@ export default function VaccinationsScreen() {
}
};
+ const handleEditVaccination = (vaccination: Vaccination) => {
+ setIsEditMode(true);
+ setEditingVaccinationId(vaccination.id);
+
+ const dateOnly = vaccination.vaccination_date.split('T')[0];
+ setVacName(vaccination.vac_name);
+ setVaccinationDate(dateOnly);
+ const expireDateOnly = vaccination.expire_date ? vaccination.expire_date.split('T')[0] : '';
+ setExpireDate(expireDateOnly);
+ setCosts(vaccination.costs ? vaccination.costs.toString() : '');
+ setNotes(vaccination.notes || '');
+ setModalVisible(true);
+ };
+
+ const handleDeleteVaccination = async (vaccination: Vaccination) => {
+ setVaccinationToDelete(vaccination);
+ setDeleteDialogVisible(true);
+ };
+
+ const confirmDeleteVaccination = async () => {
+ if (!vaccinationToDelete) return;
+
+ try {
+ const success = await vaccinationsService.deleteVaccination(vaccinationToDelete.id);
+
+ if (success) {
+ // Refresh vaccinations
+ const refreshedVaccinations = await vaccinationsService.getAllVaccinations();
+ setVaccinations(refreshedVaccinations);
+ setDeleteDialogVisible(false);
+ setVaccinationToDelete(null);
+ } else {
+ alert('Rokotuksen poistaminen epäonnistui. Yritä uudelleen.');
+ }
+ } catch (err: any) {
+ console.error("Failed to delete vaccination:", err);
+ alert('Rokotuksen poistaminen epäonnistui. Yritä uudelleen.');
+ }
+ };
+
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('fi-FI', {
@@ -211,17 +250,29 @@ export default function VaccinationsScreen() {
)}
- {vaccination.notes && (
- <>
-
+
+
+
+ {vaccination.notes ? (
{vaccination.notes}
- >
- )}
+ ) : (
+
+ )}
+
+
+ handleEditVaccination(vaccination)} style={styles.actionButton}>
+
+
+ handleDeleteVaccination(vaccination)} style={styles.actionButton}>
+
+
+
+
);
@@ -346,7 +397,7 @@ export default function VaccinationsScreen() {
contentContainerStyle={styles.scrollContentContainer}
>
- Lisää rokotus
+ {isEditMode ? 'Muokkaa rokotusta' : 'Lisää rokotus'}
+
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: COLORS.background,
- },
- tabsContainer: {
- maxHeight: 70,
- backgroundColor: COLORS.surface,
- borderBottomWidth: 1,
- borderBottomColor: COLORS.surfaceVariant,
- },
- tabsContent: {
- paddingHorizontal: SPACING.md,
- paddingVertical: SPACING.md,
- gap: SPACING.sm,
- alignItems: 'center',
- justifyContent: 'center',
- flexGrow: 1,
- },
- tab: {
- marginHorizontal: SPACING.xs,
- paddingHorizontal: SPACING.md,
- height: 40,
- justifyContent: 'center',
- alignItems: 'center',
- },
- selectedTab: {
- backgroundColor: COLORS.primary,
- elevation: 3,
- },
- selectedTabText: {
- color: '#FFFFFF',
- fontWeight: '700',
- fontSize: 15,
- lineHeight: 20,
- },
- unselectedTabText: {
- fontSize: 15,
- lineHeight: 20,
- },
- content: {
- flex: 1,
- },
- scrollContent: {
- padding: SPACING.md,
- },
- vaccinationCard: {
- marginBottom: SPACING.md,
- elevation: 2,
- },
- vaccinationHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: SPACING.sm,
- },
- vaccinationName: {
- fontWeight: '700',
- color: COLORS.primary,
- flex: 1,
- },
- dateContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING.xs,
- marginBottom: SPACING.sm,
- },
- dateText: {
- fontWeight: '600',
- color: COLORS.onSurface,
- },
- divider: {
- marginVertical: SPACING.sm,
- },
- notesContainer: {
- flexDirection: 'row',
- gap: SPACING.sm,
- marginTop: SPACING.xs,
- },
- notesText: {
- flex: 1,
- color: COLORS.onSurfaceVariant,
- fontStyle: 'italic',
- },
- emptyContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingVertical: SPACING.xl * 2,
- },
- emptyTitle: {
- marginTop: SPACING.md,
- color: COLORS.onSurfaceVariant,
- },
- emptyText: {
- marginTop: SPACING.xs,
- color: COLORS.onSurfaceVariant,
- textAlign: 'center',
- },
- fab: {
- position: 'absolute',
- right: SPACING.md,
- bottom: SPACING.md,
- backgroundColor: COLORS.primary,
- },
- modalContainer: {
- backgroundColor: COLORS.surface,
- margin: SPACING.lg,
- padding: SPACING.lg,
- borderRadius: 12,
- maxHeight: '90%',
- },
- scrollContentContainer: {
- paddingBottom: 0,
- },
- modalTitle: {
- marginBottom: SPACING.lg,
- fontWeight: 'bold',
- color: COLORS.onSurface,
- },
- input: {
- marginBottom: SPACING.md,
- },
- modalButtons: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginTop: SPACING.lg,
- gap: SPACING.md,
- },
- modalButton: {
- flex: 1,
- },
-});
diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx
index 145a1d5..e4003c4 100644
--- a/mobile/src/screens/VisitsScreen.tsx
+++ b/mobile/src/screens/VisitsScreen.tsx
@@ -1,9 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
-import { View, ScrollView, StyleSheet, TouchableOpacity, Keyboard } from 'react-native';
-import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button } from 'react-native-paper';
+import { View, ScrollView, TouchableOpacity, Keyboard } from 'react-native';
+import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, TextInput, Button, Dialog } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { visitsStyles as styles } from '../styles/screenStyles';
import { COLORS, SPACING } from '../styles/theme';
import apiClient from '../services/api';
+import { visitsService } from '../services/visitsService';
import { Pet } from '../types';
import DateTimePicker from '@react-native-community/datetimepicker';
@@ -34,6 +36,10 @@ export default function VisitsScreen() {
const [saving, setSaving] = useState(false);
const [showDatePicker, setShowDatePicker] = useState(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
+ const [editingVisitId, setEditingVisitId] = useState(null);
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [deleteDialogVisible, setDeleteDialogVisible] = useState(false);
+ const [visitToDelete, setVisitToDelete] = useState(null);
const scrollViewRef = useRef(null);
const costsInputRef = useRef(null);
@@ -76,9 +82,9 @@ export default function VisitsScreen() {
setError(null);
// Fetch pets and visits in parallel
- const [petsResponse, visitsResponse] = await Promise.all([
+ const [petsResponse, fetchedVisits] = await Promise.all([
apiClient.get('/api/pets'),
- apiClient.get('/api/vet-visits')
+ visitsService.getAllVisits()
]);
if (petsResponse.data.success && petsResponse.data.data) {
@@ -91,22 +97,7 @@ export default function VisitsScreen() {
}
}
- if (visitsResponse.data.success && visitsResponse.data.data) {
- // Flatten the nested structure: each pet has a vet_visits array
- const flattenedVisits: Visit[] = [];
- visitsResponse.data.data.forEach((petVisitGroup: any) => {
- const petId = petVisitGroup.pet_id;
- if (petVisitGroup.vet_visits && Array.isArray(petVisitGroup.vet_visits)) {
- petVisitGroup.vet_visits.forEach((visit: any) => {
- flattenedVisits.push({
- ...visit,
- pet_id: petId
- });
- });
- }
- });
- setVisits(flattenedVisits);
- }
+ setVisits(fetchedVisits);
} catch (err: any) {
console.error('Failed to fetch data:', err);
setError('Tietojen lataus epäonnistui. Yritä uudelleen.');
@@ -123,7 +114,9 @@ export default function VisitsScreen() {
.sort((a, b) => new Date(b.visit_date).getTime() - new Date(a.visit_date).getTime());
const handleOpenModal = async () => {
- // Reset form
+ // Reset form for create mode
+ setIsEditMode(false);
+ setEditingVisitId(null);
setVisitDate(new Date().toISOString().split('T')[0]);
setVetName('');
setLocation('');
@@ -135,11 +128,9 @@ export default function VisitsScreen() {
// Fetch visit types if not already loaded
if (visitTypes.length === 0) {
try {
- const typesResponse = await apiClient.get('/api/vet-visits/types');
- if (typesResponse.data.success && typesResponse.data.data) {
- console.log('Visit types response:', typesResponse.data.data);
- setVisitTypes(typesResponse.data.data);
- }
+ const types = await visitsService.getVisitTypes();
+ console.log('Visit types response:', types);
+ setVisitTypes(types);
} catch (err: any) {
console.error('Failed to fetch visit types:', err);
}
@@ -148,10 +139,12 @@ export default function VisitsScreen() {
const handleCloseModal = () => {
setModalVisible(false);
+ setIsEditMode(false);
+ setEditingVisitId(null);
};
const handleSaveVisit = async () => {
- if (!selectedPetId || !vetName || !location || !selectedTypeId) {
+ if (!vetName || !location || !selectedTypeId) {
alert('Täytä kaikki pakolliset kentät');
return;
}
@@ -159,38 +152,50 @@ export default function VisitsScreen() {
try {
setSaving(true);
- const visitData = {
- pet_id: selectedPetId,
- visit_date: visitDate,
- vet_name: vetName,
- location: location,
- type_id: selectedTypeId,
- notes: notes || undefined,
- costs: costs ? parseFloat(costs) : undefined
- };
-
- const response = await apiClient.post('/api/vet-visits', visitData);
-
- if (response.data.success) {
- // Refresh visits
- const visitsResponse = await apiClient.get('/api/vet-visits');
- if (visitsResponse.data.success && visitsResponse.data.data) {
- const flattenedVisits: Visit[] = [];
- visitsResponse.data.data.forEach((petVisitGroup: any) => {
- const petId = petVisitGroup.pet_id;
- if (petVisitGroup.vet_visits && Array.isArray(petVisitGroup.vet_visits)) {
- petVisitGroup.vet_visits.forEach((visit: any) => {
- flattenedVisits.push({
- ...visit,
- pet_id: petId
- });
- });
- }
- });
- setVisits(flattenedVisits);
+ if (isEditMode && editingVisitId) {
+
+ const visitData = {
+ visit_date: visitDate,
+ vet_name: vetName,
+ location: location,
+ type_id: selectedTypeId,
+ notes: notes || undefined,
+ costs: costs ? parseFloat(costs) : undefined
+ };
+
+ const updatedVisit = await visitsService.updateVisit(editingVisitId, visitData);
+
+ if (updatedVisit) {
+ // Refresh visits
+ const refreshedVisits = await visitsService.getAllVisits();
+ setVisits(refreshedVisits);
+ handleCloseModal();
+ }
+ } else {
+ // Create new visit
+ if (!selectedPetId) {
+ alert('Valitse lemmikki');
+ return;
}
- handleCloseModal();
+ const visitData = {
+ pet_id: selectedPetId,
+ visit_date: visitDate,
+ vet_name: vetName,
+ location: location,
+ type_id: selectedTypeId,
+ notes: notes || undefined,
+ costs: costs ? parseFloat(costs) : undefined
+ };
+
+ const newVisit = await visitsService.createVisit(visitData);
+
+ if (newVisit) {
+ // Refresh visits
+ const refreshedVisits = await visitsService.getAllVisits();
+ setVisits(refreshedVisits);
+ handleCloseModal();
+ }
}
} catch (err: any) {
console.error('Failed to save visit:', err);
@@ -200,6 +205,7 @@ export default function VisitsScreen() {
}
};
+
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('fi-FI', {
@@ -209,6 +215,67 @@ export default function VisitsScreen() {
});
};
+ const handleEditVisit = async (visit: Visit) => {
+ // Set edit mode
+ setIsEditMode(true);
+ setEditingVisitId(visit.id);
+
+ // Fetch visit types first if not already loaded
+ let types = visitTypes;
+ if (types.length === 0) {
+ try {
+ types = await visitsService.getVisitTypes();
+ setVisitTypes(types);
+ } catch (err: any) {
+ console.error('Failed to fetch visit types:', err);
+ }
+ }
+
+ // Pre-fill form with visit data
+ // Extract date only (remove timestamp if present)
+ const dateOnly = visit.visit_date.split('T')[0];
+ setVisitDate(dateOnly);
+ setVetName(visit.vet_name);
+ setLocation(visit.location);
+
+ // Find the type by name (since type_id stores the name, not ID)
+ const matchingType = types.find(t => t.name.toLowerCase() === visit.type_id.toLowerCase());
+ if (matchingType) {
+ setSelectedTypeId(matchingType.id);
+ }
+
+ setNotes(visit.notes || '');
+ setCosts(visit.costs || '');
+
+ setModalVisible(true);
+ };
+
+ const handleDeleteVisit = (visit: Visit) => {
+ setVisitToDelete(visit);
+ setDeleteDialogVisible(true);
+ };
+
+ const confirmDeleteVisit = async () => {
+ if (!visitToDelete) return;
+
+ try {
+ const success = await visitsService.deleteVisit(visitToDelete.id);
+
+ if (success) {
+ // Refresh visits
+ const refreshedVisits = await visitsService.getAllVisits();
+ setVisits(refreshedVisits);
+ setDeleteDialogVisible(false);
+ setVisitToDelete(null);
+ } else {
+ alert('Käynnin poistaminen epäonnistui.');
+ }
+ } catch (err: any) {
+ console.error('Failed to delete visit:', err);
+ alert('Käynnin poistaminen epäonnistui. Yritä uudelleen.');
+ }
+ };
+
const renderVisitCard = (visit: Visit) => (
@@ -249,17 +316,29 @@ export default function VisitsScreen() {
- {visit.notes && (
- <>
-
+
+
+
+ {visit.notes ? (
{visit.notes}
- >
- )}
+ ) : (
+
+ )}
+
+
+ handleEditVisit(visit)} style={styles.actionButton}>
+
+
+ handleDeleteVisit(visit)} style={styles.actionButton}>
+
+
+
+
);
@@ -383,7 +462,7 @@ export default function VisitsScreen() {
contentContainerStyle={styles.scrollContentContainer}
>
- Lisää käynti
+ {isEditMode ? 'Muokkaa käyntiä' : 'Lisää käynti'}
setShowDatePicker(true)}>
@@ -536,157 +615,24 @@ 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,
- },
- notesContainer: {
- flexDirection: 'row',
- gap: SPACING.sm,
- marginTop: SPACING.xs,
- },
- notesText: {
- flex: 1,
- color: COLORS.onSurfaceVariant,
- fontStyle: 'italic',
- },
- emptyContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingVertical: SPACING.xl * 2,
- },
- emptyTitle: {
- marginTop: SPACING.md,
- color: COLORS.onSurfaceVariant,
- },
- emptyText: {
- marginTop: SPACING.xs,
- color: COLORS.onSurfaceVariant,
- textAlign: 'center',
- },
- fab: {
- position: 'absolute',
- right: SPACING.md,
- bottom: SPACING.md,
- backgroundColor: COLORS.primary,
- },
- modalContainer: {
- backgroundColor: COLORS.surface,
- margin: SPACING.lg,
- padding: SPACING.lg,
- borderRadius: 12,
- maxHeight: '90%',
- },
- keyboardAvoid: {
- width: '100%',
- },
- scrollContentContainer: {
- paddingBottom: 0,
- },
- modalTitle: {
- marginBottom: SPACING.lg,
- fontWeight: 'bold',
- color: COLORS.onSurface,
- },
- input: {
- marginBottom: SPACING.md,
- },
- modalButtons: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginTop: SPACING.lg,
- gap: SPACING.md,
- },
- modalButton: {
- flex: 1,
- },
-});
diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx
index 6b46af9..0b731ca 100644
--- a/mobile/src/screens/WeightManagementScreen.tsx
+++ b/mobile/src/screens/WeightManagementScreen.tsx
@@ -1,11 +1,13 @@
import React, { useState, useEffect, useRef } from 'react';
-import { View, ScrollView, StyleSheet, Dimensions, TouchableOpacity } from 'react-native';
-import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, Button, TextInput } from 'react-native-paper';
+import { View, ScrollView, Dimensions, TouchableOpacity } from 'react-native';
+import { Text, Card, FAB, Chip, Divider, ActivityIndicator, Portal, Modal, Button, TextInput, Dialog } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import Svg, { Line, Circle } from 'react-native-svg';
import DateTimePicker from '@react-native-community/datetimepicker';
+import { weightsStyles as styles } from '../styles/screenStyles';
import { COLORS, SPACING } from '../styles/theme';
import apiClient from '../services/api';
+import { weightsService } from '../services/weightsService';
import { Pet } from '../types';
interface WeightRecord {
@@ -28,6 +30,10 @@ export default function WeightManagementScreen() {
const [modalVisible, setModalVisible] = useState(false);
const [saving, setSaving] = useState(false);
const [showDatePicker, setShowDatePicker] = useState(false);
+ const [editingWeightId, setEditingWeightId] = useState(null);
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [deleteDialogVisible, setDeleteDialogVisible] = useState(false);
+ const [weightToDelete, setWeightToDelete] = useState(null);
const scrollViewRef = useRef(null);
@@ -43,9 +49,9 @@ export default function WeightManagementScreen() {
setError(null);
// Fetch both pets and weights in parallel
- const [petsResponse, weightsResponse] = await Promise.all([
+ const [petsResponse, fetchedWeights] = await Promise.all([
apiClient.get('/api/pets'),
- apiClient.get('/api/weights')
+ weightsService.getAllWeights()
]);
if (petsResponse.data.success && petsResponse.data.data) {
@@ -58,30 +64,7 @@ export default function WeightManagementScreen() {
}
}
- if (weightsResponse.data.success && weightsResponse.data.data) {
- const weightsData = weightsResponse.data.data;
-
- // If nested structure (array of pet weight groups)
- if (Array.isArray(weightsData) && weightsData.length > 0 && weightsData[0].weights) {
- const flattenedWeights: WeightRecord[] = [];
- weightsData.forEach((petWeightGroup: any) => {
- const petId = petWeightGroup.pet_id;
- if (petWeightGroup.weights && Array.isArray(petWeightGroup.weights)) {
- petWeightGroup.weights.forEach((weight: any) => {
- flattenedWeights.push({
- ...weight,
- petId: petId,
- weight: parseFloat(weight.weight) // Convert string to number
- });
- });
- }
- });
- setWeightRecords(flattenedWeights);
- } else {
- // If flat structure
- setWeightRecords(weightsData);
- }
- }
+ setWeightRecords(fetchedWeights);
} catch (err: any) {
console.error('Failed to fetch data:', err);
setError('Tietojen lataus epäonnistui. Yritä uudelleen.');
@@ -107,6 +90,8 @@ export default function WeightManagementScreen() {
);
const handleOpenModal = () => {
+ setIsEditMode(false);
+ setEditingWeightId(null);
setWeight('');
setDate(new Date().toISOString().split('T')[0]);
setModalVisible(true);
@@ -114,51 +99,57 @@ export default function WeightManagementScreen() {
const handleCloseModal = () => {
setModalVisible(false);
+ setIsEditMode(false);
+ setEditingWeightId(null);
};
const handleSaveWeight = async () => {
- if (!selectedPetId || !weight) {
+ if (!weight) {
alert('Täytä kaikki pakolliset kentät');
return;
}
try {
setSaving(true);
-
- const weightData = {
- pet_id: selectedPetId,
- weight: parseFloat(weight),
- date: date
- };
- const response = await apiClient.post('/api/weights', weightData);
+ if (isEditMode && editingWeightId) {
- if (response.data.success) {
- const weightsResponse = await apiClient.get('/api/weights');
- if (weightsResponse.data.success && weightsResponse.data.data) {
- const weightsData = weightsResponse.data.data;
+ const weightData = {
+ pet_id: selectedPetId,
+ weight: parseFloat(weight),
+ date: date
+ };
+
+ const updatedWeight = await weightsService.updateWeight(editingWeightId, weightData);
+
+ if (updatedWeight) {
+ // Refresh weights
+ const refreshedWeights = await weightsService.getAllWeights();
+ setWeightRecords(refreshedWeights);
+
+ handleCloseModal();
+ }
+ } else {
+ if (!selectedPetId) {
+ alert('Valitse lemmikki ennen tallentamista.');
+ return;
+ }
+
+ const weightData = {
+ pet_id: selectedPetId,
+ weight: parseFloat(weight),
+ date: date
+ };
+
+ const newWeight = await weightsService.createWeight(weightData);
+
+ if (newWeight) {
+ // Refresh weights
+ const refreshedWeights = await weightsService.getAllWeights();
+ setWeightRecords(refreshedWeights);
- if (Array.isArray(weightsData) && weightsData.length > 0 && weightsData[0].weights) {
- const flattenedWeights: WeightRecord[] = [];
- weightsData.forEach((petWeightGroup: any) => {
- const petId = petWeightGroup.pet_id;
- if (petWeightGroup.weights && Array.isArray(petWeightGroup.weights)) {
- petWeightGroup.weights.forEach((weight: any) => {
- flattenedWeights.push({
- ...weight,
- petId: petId,
- weight: parseFloat(weight.weight)
- });
- });
- }
- });
- setWeightRecords(flattenedWeights);
- } else {
- setWeightRecords(weightsData);
- }
+ handleCloseModal();
}
-
- handleCloseModal();
}
} catch (err: any) {
alert('Painon tallentaminen epäonnistui. Yritä uudelleen.');
@@ -167,6 +158,43 @@ export default function WeightManagementScreen() {
}
};
+ const handleEditWeightRecord = (weightRecord: WeightRecord) => {
+ setIsEditMode(true);
+ setEditingWeightId(weightRecord.id);
+
+ // Pre-fill form with weight data
+ const dateOnly = weightRecord.date.split('T')[0];
+ setWeight(weightRecord.weight.toString());
+ setDate(dateOnly);
+ setModalVisible(true);
+ };
+
+ const handleDeleteWeightRecord = async (weightRecord: WeightRecord) => {
+ setWeightToDelete(weightRecord);
+ setDeleteDialogVisible(true);
+ };
+
+ const confirmDeleteWeightRecord = async () => {
+ if (!weightToDelete) return;
+
+ try {
+ const success = await weightsService.deleteWeight(weightToDelete.id);
+
+ if (success) {
+ // Refresh weights
+ const refreshedWeights = await weightsService.getAllWeights();
+ setWeightRecords(refreshedWeights);
+ setDeleteDialogVisible(false);
+ setWeightToDelete(null);
+ } else {
+ alert('Painomittauksen poistaminen epäonnistui. Yritä uudelleen.');
+ }
+ } catch (err: any) {
+ console.error("Failed to delete weight record:", err);
+ alert('Painomittauksen poistaminen epäonnistui. Yritä uudelleen.');
+ }
+ };
+
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('fi-FI', {
@@ -223,36 +251,29 @@ export default function WeightManagementScreen() {
)}
-
-
-
- {formatDate(record.date)}
-
-
+
- {record.measuredBy && (
- <>
-
-
-
-
- {record.measuredBy}
+
+ {record.date ? (
+
+
+
+ {formatDate(record.date)}
- >
- )}
-
- {record.notes && (
- <>
-
-
-
-
- {record.notes}
-
+ ) : (
+
+ )}
+
+
+ handleEditWeightRecord(record)} style={styles.actionButton}>
+
+
+ handleDeleteWeightRecord(record)} style={styles.actionButton}>
+
+
- >
- )}
+
);
@@ -646,7 +667,7 @@ export default function WeightManagementScreen() {
contentContainerStyle={styles.scrollContentContainer}
>
- Lisää painomittaus
+ {isEditMode ? 'Muokkaa painomittausta' : 'Lisää painomittaus'}
+
);
}
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: COLORS.background,
- },
- tabsContainer: {
- maxHeight: 70,
- backgroundColor: COLORS.surface,
- borderBottomWidth: 1,
- borderBottomColor: COLORS.surfaceVariant,
- },
- tabsContent: {
- paddingHorizontal: SPACING.md,
- paddingVertical: SPACING.md,
- gap: SPACING.sm,
- alignItems: 'center',
- justifyContent: 'center',
- flexGrow: 1,
- },
- tab: {
- marginHorizontal: SPACING.xs,
- paddingHorizontal: SPACING.md,
- height: 40,
- justifyContent: 'center',
- alignItems: 'center',
- },
- selectedTab: {
- backgroundColor: COLORS.primary,
- elevation: 3,
- },
- selectedTabText: {
- color: '#FFFFFF',
- fontWeight: '700',
- fontSize: 15,
- lineHeight: 20,
- },
- unselectedTabText: {
- fontSize: 15,
- lineHeight: 20,
- },
- content: {
- flex: 1,
- },
- scrollContent: {
- paddingBottom: 80,
- },
- graphContainer: {
- backgroundColor: COLORS.surface,
- padding: SPACING.md,
- paddingBottom: SPACING.xl,
- marginHorizontal: SPACING.md,
- marginTop: SPACING.md,
- marginBottom: SPACING.md,
- borderRadius: 12,
- elevation: 2,
- },
- graphTitle: {
- fontWeight: '600',
- color: COLORS.onSurface,
- marginBottom: SPACING.md,
- },
- graphContent: {
- flexDirection: 'row',
- },
- yAxisContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- axisTitle: {
- fontSize: 9,
- color: COLORS.onSurfaceVariant,
- fontWeight: '600',
- },
- yAxisLabels: {
- width: 50,
- height: 220,
- justifyContent: 'space-between',
- alignItems: 'flex-end',
- paddingRight: SPACING.xs,
- marginTop: -50,
- },
- yAxisMiddle: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- },
- axisLabel: {
- color: COLORS.onSurfaceVariant,
- textAlign: 'right',
- lineHeight: 12,
- },
- graphArea: {
- flex: 1,
- position: 'relative',
- },
- gridLines: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- height: 220,
- justifyContent: 'space-between',
- },
- gridLine: {
- height: 1,
- backgroundColor: COLORS.surfaceVariant,
- opacity: 0.5,
- },
- svgGraph: {
- marginBottom: SPACING.xs,
- },
- xAxisLabelsContainer: {
- flexDirection: 'row',
- justifyContent: 'space-around',
- paddingBottom: SPACING.sm,
- position: 'relative',
- height: 20,
- },
- xAxisLabel: {
- fontSize: 9,
- color: COLORS.onSurfaceVariant,
- textAlign: 'center',
- flex: 1,
- flexShrink: 1,
- },
- xAxisLabelShown: {
- fontSize: 10,
- color: COLORS.onSurfaceVariant,
- textAlign: 'center',
- flex: 1,
- },
- xAxisTitle: {
- fontSize: 10,
- color: COLORS.onSurfaceVariant,
- fontWeight: '600',
- textAlign: 'center',
- marginTop: SPACING.xs,
- },
- yearTabsContainer: {
- marginTop: SPACING.md,
- paddingTop: SPACING.sm,
- borderTopWidth: 1,
- borderTopColor: COLORS.surfaceVariant,
- },
- yearTabsContent: {
- gap: SPACING.xs,
- paddingHorizontal: 4,
- },
- yearTab: {
- backgroundColor: COLORS.surfaceVariant,
- },
- selectedYearTab: {
- backgroundColor: COLORS.primary,
- },
- selectedYearTabText: {
- color: '#FFFFFF',
- fontWeight: '600',
- },
- unselectedYearTabText: {
- color: COLORS.onSurfaceVariant,
- },
- dividerSection: {
- paddingHorizontal: SPACING.md,
- },
- sectionTitle: {
- fontWeight: '600',
- color: COLORS.onSurface,
- marginTop: SPACING.md,
- marginBottom: SPACING.xs,
- },
- weightCard: {
- marginHorizontal: SPACING.md,
- marginBottom: SPACING.md,
- elevation: 2,
- },
- weightHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: SPACING.sm,
- },
- weightMainInfo: {
- flexDirection: 'row',
- alignItems: 'baseline',
- gap: SPACING.xs,
- },
- weightValue: {
- fontWeight: '700',
- color: COLORS.primary,
- },
- weightUnit: {
- color: COLORS.onSurfaceVariant,
- fontWeight: '600',
- },
- changeChip: {
- marginLeft: SPACING.sm,
- },
- increaseChip: {
- backgroundColor: '#FFEBEE',
- },
- decreaseChip: {
- backgroundColor: '#E8F5E9',
- },
- increaseChipText: {
- color: '#D32F2F',
- },
- decreaseChipText: {
- color: '#2E7D32',
- },
- dateContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING.xs,
- marginTop: SPACING.xs,
- },
- dateText: {
- color: COLORS.onSurfaceVariant,
- },
- divider: {
- marginVertical: SPACING.sm,
- },
- weightDetail: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: SPACING.sm,
- marginBottom: SPACING.xs,
- },
- detailText: {
- flex: 1,
- color: COLORS.onSurface,
- },
- notesContainer: {
- flexDirection: 'row',
- gap: SPACING.sm,
- marginTop: SPACING.xs,
- },
- notesText: {
- flex: 1,
- color: COLORS.onSurfaceVariant,
- fontStyle: 'italic',
- },
- emptyContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingVertical: SPACING.xl * 2,
- },
- emptyTitle: {
- marginTop: SPACING.md,
- color: COLORS.onSurfaceVariant,
- },
- emptyText: {
- marginTop: SPACING.xs,
- color: COLORS.onSurfaceVariant,
- textAlign: 'center',
- },
- fab: {
- position: 'absolute',
- right: SPACING.md,
- bottom: SPACING.md,
- backgroundColor: COLORS.primary,
- },
- graphScrollView: {
- width: '100%',
- },
- modalContainer: {
- backgroundColor: COLORS.surface,
- margin: SPACING.lg,
- padding: SPACING.lg,
- borderRadius: 12,
- maxHeight: '90%',
- },
- scrollContentContainer: {
- paddingBottom: 0,
- },
- modalTitle: {
- marginBottom: SPACING.lg,
- fontWeight: 'bold',
- color: COLORS.onSurface,
- },
- input: {
- marginBottom: SPACING.md,
- },
- modalButtons: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginTop: SPACING.lg,
- gap: SPACING.md,
- },
- modalButton: {
- flex: 1,
- },
-});
diff --git a/mobile/src/services/medicationsService.ts b/mobile/src/services/medicationsService.ts
new file mode 100644
index 0000000..7defda7
--- /dev/null
+++ b/mobile/src/services/medicationsService.ts
@@ -0,0 +1,87 @@
+import apiClient from "./api";
+
+interface Medication {
+ id: number;
+ pet_id: number;
+ med_name: string;
+ medication_date: string;
+ expire_date?: string;
+ notes?: string;
+ costs?: string;
+}
+
+interface CreateMedicationData {
+ pet_id: number;
+ med_name: string;
+ medication_date: string;
+ expire_date?: string;
+ notes?: string;
+ costs?: number;
+}
+
+interface UpdateMedicationData {
+ med_name?: string;
+ medication_date?: string;
+ expire_date?: string;
+ notes?: string;
+ costs?: number;
+}
+
+export const medicationsService = {
+ // Get all medications
+ getAllMedications: async (): Promise => {
+ const response = await apiClient.get('/api/medications');
+
+ if (response.data.success && response.data.data) {
+ // Flatten the nested structure: each pet has a medications array
+ const flattenedMedications: Medication[] = [];
+ response.data.data.forEach((petMedicationGroup: any) => {
+ const petId = petMedicationGroup.pet_id;
+ if (petMedicationGroup.medications && Array.isArray(petMedicationGroup.medications)) {
+ petMedicationGroup.medications.forEach((medication: any) => {
+ flattenedMedications.push({
+ ...medication,
+ pet_id: petId
+ });
+ });
+ }
+ });
+ return flattenedMedications;
+ }
+
+ return [];
+ },
+
+ // get medications for a specific pet
+ getMedicationsByPetId: async (petId: number): Promise => {
+ const allMedications = await medicationsService.getAllMedications();
+ return allMedications.filter(medication => medication.pet_id === petId);
+ },
+
+ // Create a new medication
+ createMedication: async (medicationData: CreateMedicationData): Promise => {
+ const response = await apiClient.post('/api/medications', medicationData);
+
+ if (response.data.success && response.data.data) {
+ return response.data.data;
+ }
+ return null;
+ },
+
+ // Update an existing medication
+ updateMedication: async (medicationId: number, updatedData: UpdateMedicationData): Promise => {
+ const response = await apiClient.put(`/api/medications/${medicationId}`, updatedData);
+
+ if (response.data.success && response.data.data) {
+ return response.data.data;
+ }
+
+ return null;
+ },
+
+ // Delete a medication
+ deleteMedication: async (medicationId: number): Promise => {
+ const response = await apiClient.delete(`/api/medications/${medicationId}`);
+ return response.data.success;
+ },
+};
\ No newline at end of file
diff --git a/mobile/src/services/vaccinationsService.ts b/mobile/src/services/vaccinationsService.ts
new file mode 100644
index 0000000..cbbfd4d
--- /dev/null
+++ b/mobile/src/services/vaccinationsService.ts
@@ -0,0 +1,88 @@
+import apiClient from './api';
+
+interface Vaccination {
+ id: number;
+ pet_id: number;
+ vac_name: string;
+ vaccination_date: string;
+ expire_date?: string;
+ costs?: string;
+ notes?: string;
+}
+
+interface CreateVaccinationData {
+ pet_id: number;
+ vac_name: string;
+ vaccination_date: string;
+ expire_date?: string;
+ notes?: string;
+ costs?: number;
+}
+
+interface UpdateVaccinationData {
+ vac_name?: string;
+ vaccination_date?: string;
+ expire_date?: string;
+ notes?: string;
+ costs?: number;
+}
+
+export const vaccinationsService = {
+ // Get all vaccinations
+ getAllVaccinations: async (): Promise => {
+ const response = await apiClient.get('/api/vaccinations');
+
+ if (response.data.success && response.data.data) {
+ // Flatten the nested structure: each pet has a vaccinations array
+ const flattenedVaccinations: Vaccination[] = [];
+ response.data.data.forEach((petVaccinationGroup: any) => {
+ const petId = petVaccinationGroup.pet_id;
+ if (petVaccinationGroup.vaccinations && Array.isArray(petVaccinationGroup.vaccinations)) {
+ petVaccinationGroup.vaccinations.forEach((vaccination: any) => {
+ flattenedVaccinations.push({
+ ...vaccination,
+ pet_id: petId
+ });
+ });
+ }
+ });
+ return flattenedVaccinations;
+ }
+
+ return [];
+ },
+
+ // Get vaccinations for a specific pet
+ getVaccinationsByPetId: async (petId: number): Promise => {
+ const allVaccinations = await vaccinationsService.getAllVaccinations();
+ return allVaccinations.filter(vaccination => vaccination.pet_id === petId);
+ },
+
+ // Create a new vaccination
+ createVaccination: async (vaccinationData: CreateVaccinationData): Promise => {
+ const response = await apiClient.post('/api/vaccinations', vaccinationData);
+
+ if (response.data.success) {
+ return response.data.data;
+ }
+
+ return null;
+ },
+
+ // Update a vaccination
+ updateVaccination: async (vaccinationId: number, vaccinationData: UpdateVaccinationData): Promise => {
+ const response = await apiClient.put(`/api/vaccinations/${vaccinationId}`, vaccinationData);
+
+ if (response.data.success) {
+ return response.data.data;
+ }
+
+ return null;
+ },
+
+ // Delete a vaccination
+ deleteVaccination: async (vaccinationId: number): Promise => {
+ const response = await apiClient.delete(`/api/vaccinations/${vaccinationId}`);
+ return response.data.success;
+ },
+};
diff --git a/mobile/src/services/visitsService.ts b/mobile/src/services/visitsService.ts
new file mode 100644
index 0000000..dcdbad7
--- /dev/null
+++ b/mobile/src/services/visitsService.ts
@@ -0,0 +1,107 @@
+import apiClient from './api';
+
+interface Visit {
+ id: number;
+ pet_id: number;
+ visit_date: string;
+ location: string;
+ vet_name: string;
+ type_id: string;
+ notes?: string;
+ costs?: string;
+}
+
+interface VisitType {
+ id: number;
+ name: string;
+}
+
+interface CreateVisitData {
+ pet_id: number;
+ visit_date: string;
+ vet_name: string;
+ location: string;
+ type_id: number;
+ notes?: string;
+ costs?: number;
+}
+
+interface UpdateVisitData {
+ visit_date?: string;
+ vet_name?: string;
+ location?: string;
+ type_id?: number;
+ notes?: string;
+ costs?: number;
+}
+
+export const visitsService = {
+ // Get all visits
+ getAllVisits: async (): Promise => {
+ const response = await apiClient.get('/api/vet-visits');
+
+ if (response.data.success && response.data.data) {
+ // Flatten the nested structure: each pet has a vet_visits array
+ const flattenedVisits: Visit[] = [];
+ response.data.data.forEach((petVisitGroup: any) => {
+ const petId = petVisitGroup.pet_id;
+ if (petVisitGroup.vet_visits && Array.isArray(petVisitGroup.vet_visits)) {
+ petVisitGroup.vet_visits.forEach((visit: any) => {
+ flattenedVisits.push({
+ ...visit,
+ pet_id: petId
+ });
+ });
+ }
+ });
+ return flattenedVisits;
+ }
+
+ return [];
+ },
+
+ // Get visits for a specific pet
+ getVisitsByPetId: async (petId: number): Promise => {
+ const allVisits = await visitsService.getAllVisits();
+ return allVisits.filter(visit => visit.pet_id === petId);
+ },
+
+ // Get visit types
+ getVisitTypes: async (): Promise => {
+ const response = await apiClient.get('/api/vet-visits/types');
+
+ if (response.data.success && response.data.data) {
+ return response.data.data;
+ }
+
+ return [];
+ },
+
+ // Create a new visit
+ createVisit: async (visitData: CreateVisitData): Promise => {
+ const response = await apiClient.post('/api/vet-visits', visitData);
+
+ if (response.data.success) {
+ return response.data.data;
+ }
+
+ return null;
+ },
+
+ // Update a visit
+ updateVisit: async (visitId: number, visitData: UpdateVisitData): Promise => {
+ const response = await apiClient.put(`/api/vet-visits/${visitId}`, visitData);
+
+ if (response.data.success) {
+ return response.data.data;
+ }
+
+ return null;
+ },
+
+ // Delete a visit
+ deleteVisit: async (visitId: number): Promise => {
+ const response = await apiClient.delete(`/api/vet-visits/${visitId}`);
+ return response.data.success;
+ },
+};
diff --git a/mobile/src/services/weightsService.ts b/mobile/src/services/weightsService.ts
new file mode 100644
index 0000000..6a50537
--- /dev/null
+++ b/mobile/src/services/weightsService.ts
@@ -0,0 +1,94 @@
+import apiClient from './api';
+
+interface WeightRecord {
+ id: number;
+ petId: number;
+ date: string;
+ weight: number;
+ created_at: string;
+ notes?: string;
+ measuredBy?: string;
+}
+
+interface CreateWeightData {
+ pet_id: number;
+ weight: number;
+ date: string;
+ notes?: string;
+ measuredBy?: string;
+}
+
+interface UpdateWeightData {
+ weight?: number;
+ date?: string;
+ notes?: string;
+ measuredBy?: string;
+}
+
+export const weightsService = {
+ // Get all weight records
+ getAllWeights: async (): Promise => {
+ const response = await apiClient.get('/api/weights');
+
+ if (response.data.success && response.data.data) {
+ const weightsData = response.data.data;
+
+ // If nested structure (array of pet weight groups)
+ if (Array.isArray(weightsData) && weightsData.length > 0 && weightsData[0].weights) {
+ const flattenedWeights: WeightRecord[] = [];
+ weightsData.forEach((petWeightGroup: any) => {
+ const petId = petWeightGroup.pet_id;
+ if (petWeightGroup.weights && Array.isArray(petWeightGroup.weights)) {
+ petWeightGroup.weights.forEach((weight: any) => {
+ flattenedWeights.push({
+ ...weight,
+ petId: petId,
+ weight: parseFloat(weight.weight) // Convert string to number
+ });
+ });
+ }
+ });
+ return flattenedWeights;
+ } else {
+ // If flat structure
+ return weightsData;
+ }
+ }
+
+ return [];
+ },
+
+ // Get weight records for a specific pet
+ getWeightsByPetId: async (petId: number): Promise => {
+ const allWeights = await weightsService.getAllWeights();
+ return allWeights.filter(weight => weight.petId === petId);
+ },
+
+ // Create a new weight record
+ createWeight: async (weightData: CreateWeightData): Promise => {
+ const response = await apiClient.post('/api/weights', weightData);
+
+ if (response.data.success) {
+ return response.data.data;
+ }
+
+ return null;
+ },
+
+ // Update a weight record
+ updateWeight: async (weightId: number, weightData: UpdateWeightData): Promise => {
+ const response = await apiClient.put(`/api/weights/${weightId}`, weightData);
+
+ if (response.data.success) {
+ return response.data.data;
+ }
+
+ return null;
+ },
+
+ // Delete a weight record
+ deleteWeight: async (weightId: number): Promise => {
+ const response = await apiClient.delete(`/api/weights/${weightId}`);
+ return response.data.success;
+ },
+};
diff --git a/mobile/src/styles/authStyles.ts b/mobile/src/styles/authStyles.ts
index 1f1317c..569fd2a 100644
--- a/mobile/src/styles/authStyles.ts
+++ b/mobile/src/styles/authStyles.ts
@@ -78,6 +78,7 @@ export const authStyles = StyleSheet.create({
...COMMON_STYLES.buttonContent,
},
+
// Links
forgotPassword: {
...TYPOGRAPHY.bodyMedium,
diff --git a/mobile/src/styles/screenStyles.ts b/mobile/src/styles/screenStyles.ts
index 1915a9c..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
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,
},