diff --git a/messages/de.json b/messages/de.json index 8bb2219..2e5e6e3 100644 --- a/messages/de.json +++ b/messages/de.json @@ -482,7 +482,8 @@ "typeAdoptionDay": "🏠 Heute ist der Adoptionstag! Feiere den Jahrestag.", "typePetSuggestion": "könnte dir gefallen", "typeReferralJoined": "ist über deine Einladung zu PawFeed gekommen", - "typeReferralRankUp": "Du hast einen neuen Empfehlungsrang erreicht. Schau ihn dir an!" + "typeReferralRankUp": "Du hast einen neuen Empfehlungsrang erreicht. Schau ihn dir an!", + "typeVaccineDue": "💉 Die Impfung „{name}“ ist am {date} fällig – Zeit, einen Termin zu vereinbaren." }, "PhotoPostForm": { "captionPlaceholder": "Bildunterschrift schreiben…", diff --git a/messages/en.json b/messages/en.json index 697af95..37d7a9b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -482,7 +482,8 @@ "typeAdoptionDay": "🏠 Today is adoption day! Celebrate the anniversary.", "typePetSuggestion": "might be a good match", "typeReferralJoined": "joined PawFeed through your invite", - "typeReferralRankUp": "You reached a new referral rank. Take a look!" + "typeReferralRankUp": "You reached a new referral rank. Take a look!", + "typeVaccineDue": "💉 The \"{name}\" vaccination is due on {date} – time to book an appointment." }, "PhotoPostForm": { "captionPlaceholder": "Write a caption…", diff --git a/mobile/app/(app)/(tabs)/notifications.tsx b/mobile/app/(app)/(tabs)/notifications.tsx index 25bd357..f765089 100644 --- a/mobile/app/(app)/(tabs)/notifications.tsx +++ b/mobile/app/(app)/(tabs)/notifications.tsx @@ -9,6 +9,7 @@ import { useActivePet } from "../../../src/context/ActivePetContext"; import { useTheme } from "../../../src/context/ThemeContext"; import { PetAvatar } from "../../../src/components/PetAvatar"; import { formatPetName, formatRelativeTime } from "../../../src/lib/format"; +import { isoToDisplayDate } from "../../../src/lib/date-format"; import { getTypography, type ColorTokens, type Typography } from "../../../src/theme/tokens"; import type { NotificationItem } from "../../../src/api/types"; @@ -37,6 +38,11 @@ const SELF_MESSAGE_KEYS: Partial> = { // independently of the row's own tap target, ported from web's // notifications/page.tsx (name as a , rest as plain text). function getNotificationSuffix(item: NotificationItem, t: TFunction): string { + if (item.type === "VACCINE_DUE") { + if (!item.vaccine?.nextDueDate) return t("notifications.vaccineDueGeneric"); + const iso = new Date(item.vaccine.nextDueDate).toISOString().slice(0, 10); + return t("notifications.vaccineDue", { name: item.vaccine.name, date: isoToDisplayDate(iso) }); + } const selfKey = SELF_MESSAGE_KEYS[item.type]; if (selfKey) return t(selfKey); const actorKey = ACTOR_MESSAGE_KEYS[item.type]; @@ -44,6 +50,11 @@ function getNotificationSuffix(item: NotificationItem, t: TFunction): string { return item.type; } +const SELF_ICONS: Partial> = { + BIRTHDAY: "🎂", + VACCINE_DUE: "💉", +}; + /** * Port of web's notifications/page.tsx getHref()/getPostId(): FOLLOW goes to * the actor's profile, BIRTHDAY/ADOPTION_DAY to this pet's own health page @@ -64,6 +75,7 @@ function getNotificationRoute(item: NotificationItem): string | null { return "/invite"; case "BIRTHDAY": case "ADOPTION_DAY": + case "VACCINE_DUE": return item.recipientPetId ? `/pets/${item.recipientPetId}/health` : null; case "REACTION": case "COMMENT": @@ -160,7 +172,7 @@ export default function NotificationsScreen() { ) : ( - {item.type === "BIRTHDAY" ? "🎂" : "🏠"} + {SELF_ICONS[item.type] ?? "🏠"} )} diff --git a/mobile/app/(app)/pets/[petId]/edit.tsx b/mobile/app/(app)/pets/[petId]/edit.tsx index d1461cc..fcb5e77 100644 --- a/mobile/app/(app)/pets/[petId]/edit.tsx +++ b/mobile/app/(app)/pets/[petId]/edit.tsx @@ -8,6 +8,7 @@ import { useTRPC } from "../../../../src/api/trpc"; import { useActivePet } from "../../../../src/context/ActivePetContext"; import { useTheme } from "../../../../src/context/ThemeContext"; import { TextField } from "../../../../src/components/TextField"; +import { DateField } from "../../../../src/components/DateField"; import { Button } from "../../../../src/components/Button"; import { BreedPicker } from "../../../../src/components/pet/BreedPicker"; import { radii, getTypography, type ColorTokens, type Typography } from "../../../../src/theme/tokens"; @@ -156,10 +157,10 @@ export default function EditPetScreen() { /> {t("editPet.birthdayLabel")} - + {t("editPet.adoptedAtLabel")} - + {error ? {error} : null} diff --git a/mobile/src/components/DateField.tsx b/mobile/src/components/DateField.tsx new file mode 100644 index 0000000..a0bab14 --- /dev/null +++ b/mobile/src/components/DateField.tsx @@ -0,0 +1,25 @@ +import type { TextInputProps } from "react-native"; +import { TextField } from "./TextField"; +import { formatDateInput } from "../lib/date-format"; + +const DISPLAY_DATE_LENGTH = 10; // DD.MM.YYYY + +type DateFieldProps = Omit & { + onChangeText: (value: string) => void; +}; + +/** + * Text date input with a DD.MM.YYYY mask: number pad only, the dots are + * inserted automatically (iOS' number pad has no "." key). A native wheel + * picker needs @react-native-community/datetimepicker → PENDING-NATIVE-BUILD.md. + */ +export function DateField({ onChangeText, ...props }: DateFieldProps) { + return ( + onChangeText(formatDateInput(text))} + /> + ); +} diff --git a/mobile/src/components/health/VaccineSection.tsx b/mobile/src/components/health/VaccineSection.tsx index 5104b90..779314a 100644 --- a/mobile/src/components/health/VaccineSection.tsx +++ b/mobile/src/components/health/VaccineSection.tsx @@ -6,10 +6,14 @@ import { Plus, Trash2 } from "lucide-react-native"; import { useTRPC } from "../../api/trpc"; import { useTheme } from "../../context/ThemeContext"; import { TextField } from "../TextField"; +import { DateField } from "../DateField"; import { Button } from "../Button"; import { Card } from "../Card"; import { getTypography, type ColorTokens, type Typography } from "../../theme/tokens"; -import { displayDateToIso, todayAsDisplayDate } from "../../lib/date-format"; +import { addYearsToDisplayDate, displayDateToIso, todayAsDisplayDate } from "../../lib/date-format"; + +/** Übliche Auffrisch-Intervalle (z. B. Tollwut 1–3 Jahre) als Schnellauswahl für den Folgetermin. */ +const NEXT_DUE_PRESET_YEARS = [1, 3]; /** Port of web's health.ts vaccine CRUD — see health/page.tsx's Impfungen tab. */ export function VaccineSection({ petId }: { petId: string }) { @@ -80,8 +84,24 @@ export function VaccineSection({ petId }: { petId: string }) { {showForm && ( - - + + + + {NEXT_DUE_PRESET_YEARS.map((years) => { + const suggestion = addYearsToDisplayDate(dateGiven, years); + return ( + suggestion && setNextDueDate(suggestion)} + > + {t("health.vaccineNextDuePreset", { count: years })} + + ); + })} + + {t("health.vaccineReminderHint")}