Replace the profile tab with prescription management (inline reminders and family invite), add a shared tab top bar, separate notifications from orders, and support any-branch guest checkout with GSH codes. Co-authored-by: Cursor <cursoragent@cursor.com>
203 lines
5.9 KiB
TypeScript
203 lines
5.9 KiB
TypeScript
import { useMemo, useState } from "react";
|
||
import { Alert, View } from "react-native";
|
||
import { Badge } from "@/components/ui/Badge";
|
||
import { Button } from "@/components/ui/Button";
|
||
import { Chip } from "@/components/ui/Chip";
|
||
import { SwitchRow } from "@/components/ui/Field";
|
||
import { Text } from "@/components/ui/Text";
|
||
import { scheduleDailyDoseReminders } from "@/lib/notifications";
|
||
import { useEmergencyContactStore } from "@/lib/stores/emergencyContactStore";
|
||
import { useNotificationStore } from "@/lib/stores/notificationStore";
|
||
import {
|
||
usePrescriptionStore,
|
||
type Prescription,
|
||
type Reminder,
|
||
} from "@/lib/stores/prescriptionStore";
|
||
import { colors, radii } from "@/lib/theme";
|
||
import { routes } from "@/lib/routes";
|
||
|
||
const PRESETS = ["08:00", "12:00", "14:00", "18:00", "20:00"] as const;
|
||
|
||
export function DoseReminderPanel({
|
||
rx,
|
||
reminder,
|
||
compact,
|
||
onScheduled,
|
||
}: {
|
||
rx: Prescription;
|
||
reminder?: Reminder | null;
|
||
compact?: boolean;
|
||
onScheduled?: () => void;
|
||
}) {
|
||
const upsertReminder = usePrescriptionStore((s) => s.upsertReminder);
|
||
const schedulePendingDose = usePrescriptionStore((s) => s.schedulePendingDose);
|
||
const grace = useEmergencyContactStore((s) => s.graceMinutesDefault);
|
||
const escalationEnabled = useEmergencyContactStore((s) => s.escalationEnabled);
|
||
const setEscalationEnabled = useEmergencyContactStore((s) => s.setEscalationEnabled);
|
||
const contacts = useEmergencyContactStore((s) => s.contacts);
|
||
const pushNotification = useNotificationStore((s) => s.push);
|
||
|
||
const [times, setTimes] = useState(
|
||
reminder?.times?.length ? reminder.times.join(",") : "08:00,20:00"
|
||
);
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
const selected = useMemo(
|
||
() =>
|
||
times
|
||
.split(",")
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
[times]
|
||
);
|
||
|
||
const togglePreset = (t: string) => {
|
||
if (selected.includes(t)) {
|
||
setTimes(selected.filter((x) => x !== t).join(","));
|
||
} else {
|
||
setTimes([...selected, t].sort().join(","));
|
||
}
|
||
};
|
||
|
||
const save = async () => {
|
||
if (!selected.length) {
|
||
Alert.alert("Pick a time", "Choose at least one dose time.");
|
||
return;
|
||
}
|
||
setBusy(true);
|
||
try {
|
||
const reminderId = `rem_${rx.id}`;
|
||
upsertReminder({
|
||
id: reminderId,
|
||
prescriptionId: rx.id,
|
||
medicineName: rx.medicineName,
|
||
times: selected,
|
||
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
|
||
graceMinutes: grace,
|
||
escalateOnMiss: escalationEnabled,
|
||
enabled: true,
|
||
memberId: rx.memberId,
|
||
});
|
||
|
||
const result = await scheduleDailyDoseReminders({
|
||
prescriptionId: rx.id,
|
||
medicineName: rx.medicineName,
|
||
times: selected,
|
||
});
|
||
|
||
schedulePendingDose(reminderId);
|
||
|
||
pushNotification({
|
||
kind: "dose",
|
||
title: `Reminders on · ${rx.medicineName}`,
|
||
body: result.granted
|
||
? `Daily push at ${result.times.join(", ")}. Demo ping in ~12s.`
|
||
: `Saved ${result.times.join(", ")}. Enable notification permission for push.`,
|
||
href: routes.rxCalendar(rx.id),
|
||
});
|
||
|
||
Alert.alert(
|
||
result.granted ? "Reminders scheduled" : "Reminders saved",
|
||
result.granted
|
||
? `Calendar push notifications set for ${result.times.join(", ")}. You'll get a demo notification shortly.`
|
||
: `Times saved (${result.times.join(", ")}). Allow notifications to receive daily push alerts.`
|
||
);
|
||
onScheduled?.();
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<View
|
||
style={
|
||
compact
|
||
? {
|
||
marginTop: 12,
|
||
paddingTop: 12,
|
||
borderTopWidth: 1,
|
||
borderTopColor: colors.line,
|
||
gap: 10,
|
||
}
|
||
: { gap: 12 }
|
||
}
|
||
>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||
<Text variant="h3" style={{ flex: 1 }}>
|
||
Dose times
|
||
</Text>
|
||
{reminder?.enabled ? <Badge label="Active" tone="brand" /> : <Badge label="Not set" tone="neutral" />}
|
||
</View>
|
||
{!compact ? (
|
||
<Text variant="caption" color={colors.muted}>
|
||
Pick daily times. We’ll schedule calendar push notifications for each slot.
|
||
</Text>
|
||
) : null}
|
||
|
||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
|
||
{PRESETS.map((p) => (
|
||
<Chip
|
||
key={p}
|
||
label={p}
|
||
selected={selected.includes(p)}
|
||
onPress={() => togglePreset(p)}
|
||
size="sm"
|
||
/>
|
||
))}
|
||
</View>
|
||
|
||
{selected.length ? (
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
flexWrap: "wrap",
|
||
gap: 6,
|
||
padding: 10,
|
||
borderRadius: radii.md,
|
||
backgroundColor: colors.brandWash,
|
||
borderWidth: 1,
|
||
borderColor: colors.brandBorder,
|
||
}}
|
||
>
|
||
<Text variant="caption" color={colors.brandText}>
|
||
Schedule: {selected.join(" · ")} every day
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
|
||
{!compact ? (
|
||
<View
|
||
style={{
|
||
borderRadius: radii.md,
|
||
borderWidth: 1,
|
||
borderColor: colors.line,
|
||
overflow: "hidden",
|
||
backgroundColor: colors.surface,
|
||
}}
|
||
>
|
||
<SwitchRow
|
||
icon="notifications-outline"
|
||
title="Escalate if missed"
|
||
subtitle={
|
||
contacts[0]
|
||
? `Notify ${contacts[0].name} after grace`
|
||
: "Add an emergency contact in profile"
|
||
}
|
||
value={escalationEnabled}
|
||
onValueChange={setEscalationEnabled}
|
||
last
|
||
/>
|
||
</View>
|
||
) : null}
|
||
|
||
<Button
|
||
title={busy ? "Scheduling…" : "Save & enable push"}
|
||
icon="alarm-outline"
|
||
onPress={save}
|
||
disabled={busy}
|
||
size={compact ? "md" : "lg"}
|
||
/>
|
||
</View>
|
||
);
|
||
}
|