Add payment/notification prefs, assisted pharmacist codes, Rx status journey, and mock payment intents so demo flows match the storefront brief. Co-authored-by: Cursor <cursoragent@cursor.com>
255 lines
9.1 KiB
TypeScript
255 lines
9.1 KiB
TypeScript
import { router } from "expo-router";
|
|
import { useMemo, useState } from "react";
|
|
import { Alert, ScrollView, View } from "react-native";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FamilyInviteCard } from "@/components/care";
|
|
import { RxCareCard } from "@/components/prescription";
|
|
import { Badge } from "@/components/ui/Badge";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card, Section } from "@/components/ui/Card";
|
|
import { Chip } from "@/components/ui/Chip";
|
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
import { Screen, TAB_BAR_INSET } from "@/components/ui/Screen";
|
|
import { TabTopBar } from "@/components/ui/TabTopBar";
|
|
import { Text } from "@/components/ui/Text";
|
|
import { routes } from "@/lib/routes";
|
|
import { useFamilyStore } from "@/lib/stores/familyStore";
|
|
import {
|
|
usePrescriptionStore,
|
|
type Reminder,
|
|
type RxStatus,
|
|
} from "@/lib/stores/prescriptionStore";
|
|
import { colors, gutter } from "@/lib/theme";
|
|
|
|
type Filter = "all" | RxStatus;
|
|
|
|
export default function PrescriptionsTabScreen() {
|
|
const { t } = useTranslation();
|
|
const prescriptions = usePrescriptionStore((s) => s.prescriptions);
|
|
const reminders = usePrescriptionStore((s) => s.reminders);
|
|
const members = useFamilyStore((s) => s.members);
|
|
const inviteCode = useFamilyStore((s) => s.inviteCode);
|
|
const acceptInvite = useFamilyStore((s) => s.acceptInvite);
|
|
const removeMember = useFamilyStore((s) => s.removeMember);
|
|
const setActiveMember = useFamilyStore((s) => s.setActiveMember);
|
|
const activeMemberId = useFamilyStore((s) => s.activeMemberId);
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
const [filter, setFilter] = useState<Filter>("all");
|
|
|
|
const reminderByRx = useMemo(() => {
|
|
const map = new Map<string, Reminder>();
|
|
reminders.forEach((r) => map.set(r.prescriptionId, r));
|
|
return map;
|
|
}, [reminders]);
|
|
|
|
const activeReminderCount = reminders.filter((r) => r.enabled).length;
|
|
const needingSetup = prescriptions.filter((p) => !reminderByRx.get(p.id)?.enabled).length;
|
|
const queriedCount = prescriptions.filter((p) => p.status === "queried").length;
|
|
|
|
const visible = useMemo(
|
|
() =>
|
|
filter === "all"
|
|
? prescriptions
|
|
: prescriptions.filter((p) => p.status === filter),
|
|
[prescriptions, filter]
|
|
);
|
|
|
|
return (
|
|
<Screen>
|
|
<TabTopBar subtitle={t("common.prescriptions")} />
|
|
<ScrollView
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={{
|
|
paddingBottom: TAB_BAR_INSET + 24,
|
|
paddingHorizontal: gutter,
|
|
gap: 14,
|
|
}}
|
|
>
|
|
<View>
|
|
<Text variant="h1">{t("common.prescriptions")}</Text>
|
|
<Text variant="bodySm" color={colors.subtle} style={{ marginTop: 4 }}>
|
|
Manage Rx, dose calendars, and family care shares
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={{ flexDirection: "row", gap: 10 }}>
|
|
<Card elevated={false} tone="brand" style={{ flex: 1 }}>
|
|
<Text variant="caption" color={colors.muted}>
|
|
Active reminders
|
|
</Text>
|
|
<Text variant="h1" style={{ marginTop: 2 }}>
|
|
{activeReminderCount}
|
|
</Text>
|
|
</Card>
|
|
<Card elevated={false} style={{ flex: 1 }}>
|
|
<Text variant="caption" color={colors.muted}>
|
|
Need setup
|
|
</Text>
|
|
<Text variant="h1" style={{ marginTop: 2 }}>
|
|
{needingSetup}
|
|
</Text>
|
|
</Card>
|
|
{queriedCount ? (
|
|
<Card elevated={false} style={{ flex: 1 }}>
|
|
<Text variant="caption" color={colors.muted}>
|
|
Need reply
|
|
</Text>
|
|
<Text variant="h1" style={{ marginTop: 2 }}>
|
|
{queriedCount}
|
|
</Text>
|
|
</Card>
|
|
) : null}
|
|
</View>
|
|
|
|
<Button
|
|
title={t("common.scanRx")}
|
|
icon="scan-outline"
|
|
onPress={() => router.push(routes.rxScan)}
|
|
/>
|
|
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8 }}>
|
|
{(
|
|
[
|
|
["all", "All"],
|
|
["queried", "Queried"],
|
|
["under_review", "Review"],
|
|
["approved", "Approved"],
|
|
["filled", "Filled"],
|
|
] as [Filter, string][]
|
|
).map(([id, label]) => (
|
|
<Chip
|
|
key={id}
|
|
label={label}
|
|
selected={filter === id}
|
|
onPress={() => setFilter(id)}
|
|
size="sm"
|
|
/>
|
|
))}
|
|
</ScrollView>
|
|
|
|
<Section title="Your prescriptions">
|
|
{visible.length === 0 ? (
|
|
<EmptyState
|
|
compact
|
|
icon="document-text-outline"
|
|
title={filter === "all" ? "No prescriptions yet" : "Nothing in this filter"}
|
|
body={
|
|
filter === "all"
|
|
? "Scan or upload a prescription to start reminders."
|
|
: "Try another status filter."
|
|
}
|
|
actionTitle="Scan prescription"
|
|
onAction={() => router.push(routes.rxScan)}
|
|
/>
|
|
) : (
|
|
<View style={{ gap: 12 }}>
|
|
{visible.map((rx) => (
|
|
<RxCareCard
|
|
key={rx.id}
|
|
rx={rx}
|
|
reminder={reminderByRx.get(rx.id)}
|
|
expanded={expandedId === rx.id}
|
|
onToggleExpand={() =>
|
|
setExpandedId((cur) => (cur === rx.id ? null : rx.id))
|
|
}
|
|
/>
|
|
))}
|
|
</View>
|
|
)}
|
|
</Section>
|
|
|
|
<Section title={t("common.family")}>
|
|
<FamilyInviteCard
|
|
inviteCode={inviteCode}
|
|
onInvite={() => router.push(routes.familyInvite)}
|
|
/>
|
|
|
|
{members.length === 0 ? (
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 8 }}>
|
|
No family members yet. Invite someone to share Rx care.
|
|
</Text>
|
|
) : (
|
|
<View style={{ gap: 10, marginTop: 4 }}>
|
|
{members.map((m) => {
|
|
const active = activeMemberId === m.id;
|
|
return (
|
|
<Card
|
|
key={m.id}
|
|
onPress={() => setActiveMember(active ? null : m.id)}
|
|
style={{
|
|
borderColor: active ? colors.brand : colors.line,
|
|
borderWidth: active ? 1.5 : 1,
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: "row", gap: 12, alignItems: "center" }}>
|
|
<View
|
|
style={{
|
|
width: 42,
|
|
height: 42,
|
|
borderRadius: 21,
|
|
backgroundColor: colors.brandSoft,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<Text variant="label" color={colors.brandText}>
|
|
{m.name.slice(0, 1).toUpperCase()}
|
|
</Text>
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text variant="h3">{m.name}</Text>
|
|
<Text variant="caption" color={colors.subtle} style={{ marginTop: 2 }}>
|
|
{m.relationship}
|
|
{m.phone ? ` · ${m.phone}` : ""}
|
|
{active ? " · Active for Rx" : ""}
|
|
</Text>
|
|
</View>
|
|
<Badge
|
|
label={m.status}
|
|
tone={m.status === "accepted" ? "brand" : "warn"}
|
|
/>
|
|
</View>
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
flexWrap: "wrap",
|
|
gap: 8,
|
|
marginTop: 12,
|
|
}}
|
|
>
|
|
{m.status === "pending" ? (
|
|
<Button
|
|
title="Accept"
|
|
size="sm"
|
|
fullWidth={false}
|
|
onPress={() => acceptInvite(m.id)}
|
|
/>
|
|
) : null}
|
|
<Button
|
|
title="Remove"
|
|
size="sm"
|
|
variant="ghost"
|
|
fullWidth={false}
|
|
onPress={() =>
|
|
Alert.alert("Remove member?", m.name, [
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Remove",
|
|
style: "destructive",
|
|
onPress: () => removeMember(m.id),
|
|
},
|
|
])
|
|
}
|
|
/>
|
|
</View>
|
|
</Card>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</Section>
|
|
</ScrollView>
|
|
</Screen>
|
|
);
|
|
}
|