Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline. Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
7.0 KiB
TypeScript
212 lines
7.0 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import { router, useLocalSearchParams } from "expo-router";
|
|
import { Image, ScrollView, View } from "react-native";
|
|
import { Badge } from "@/components/ui/Badge";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card, InfoRow, Section } from "@/components/ui/Card";
|
|
import { AppHeader } from "@/components/ui/Header";
|
|
import { Screen } from "@/components/ui/Screen";
|
|
import { Text } from "@/components/ui/Text";
|
|
import { products } from "@/lib/data/products";
|
|
import { useCartStore } from "@/lib/stores/cartStore";
|
|
import { usePrescriptionStore } from "@/lib/stores/prescriptionStore";
|
|
import { colors, gutter, radii } from "@/lib/theme";
|
|
import { routes } from "@/lib/routes";
|
|
|
|
const statusTone = {
|
|
submitted: "neutral",
|
|
under_review: "warn",
|
|
approved: "brand",
|
|
queried: "info",
|
|
rejected: "danger",
|
|
filled: "brand",
|
|
} as const;
|
|
|
|
const lineTone = {
|
|
ready: "brand",
|
|
review: "warn",
|
|
unavailable: "danger",
|
|
} as const;
|
|
|
|
export default function PrescriptionDetailScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const rx = usePrescriptionStore((s) => s.prescriptions.find((p) => p.id === id));
|
|
const reminders = usePrescriptionStore((s) =>
|
|
s.reminders.filter((r) => r.prescriptionId === id)
|
|
);
|
|
const markRefilled = usePrescriptionStore((s) => s.markRefilled);
|
|
const updateRxStatus = usePrescriptionStore((s) => s.updateRxStatus);
|
|
const addProduct = useCartStore((s) => s.addProduct);
|
|
|
|
if (!rx) {
|
|
return (
|
|
<Screen>
|
|
<AppHeader title="Prescription" />
|
|
<View style={{ padding: gutter }}>
|
|
<Text variant="body" color={colors.muted}>
|
|
Prescription not found.
|
|
</Text>
|
|
</View>
|
|
</Screen>
|
|
);
|
|
}
|
|
|
|
const dueIn = (() => {
|
|
if (!rx.lastFilledAt) return null;
|
|
const due = new Date(rx.lastFilledAt);
|
|
due.setDate(due.getDate() + rx.daysSupply);
|
|
return Math.ceil((due.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
})();
|
|
|
|
return (
|
|
<Screen>
|
|
<AppHeader title="Prescription detail" />
|
|
<ScrollView
|
|
contentContainerStyle={{
|
|
paddingHorizontal: gutter,
|
|
paddingBottom: 40,
|
|
gap: 16,
|
|
}}
|
|
>
|
|
{rx.imageUris[0] ? (
|
|
<Image
|
|
source={{ uri: rx.imageUris[0] }}
|
|
style={{
|
|
width: "100%",
|
|
height: 180,
|
|
borderRadius: radii.card,
|
|
backgroundColor: colors.surfaceAlt,
|
|
}}
|
|
resizeMode="cover"
|
|
/>
|
|
) : null}
|
|
|
|
<Card>
|
|
<View style={{ flexDirection: "row", justifyContent: "space-between", gap: 12 }}>
|
|
<View style={{ flex: 1 }}>
|
|
<Text variant="h1">{rx.medicineName}</Text>
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 6 }}>
|
|
For {rx.patientName}
|
|
</Text>
|
|
</View>
|
|
<Badge
|
|
label={rx.status.replace("_", " ")}
|
|
tone={statusTone[rx.status]}
|
|
/>
|
|
</View>
|
|
{dueIn !== null ? (
|
|
<Text
|
|
variant="label"
|
|
color={dueIn <= 3 ? colors.warn : colors.brandText}
|
|
style={{ marginTop: 12 }}
|
|
>
|
|
{dueIn <= 0 ? "Refill overdue" : `Refill in ${dueIn} days`}
|
|
</Text>
|
|
) : (
|
|
<Text variant="label" color={colors.warn} style={{ marginTop: 12 }}>
|
|
Awaiting first fill
|
|
</Text>
|
|
)}
|
|
</Card>
|
|
|
|
<Section title="Medicines on this Rx">
|
|
<Card elevated={false}>
|
|
{rx.medicines.map((m, i) => (
|
|
<View
|
|
key={m.name}
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingVertical: 12,
|
|
borderBottomWidth: i === rx.medicines.length - 1 ? 0 : 1,
|
|
borderBottomColor: colors.line,
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
<Ionicons name="medkit-outline" size={18} color={colors.brandText} />
|
|
<Text variant="h3">{m.name}</Text>
|
|
</View>
|
|
<Badge label={m.status} tone={lineTone[m.status]} />
|
|
</View>
|
|
))}
|
|
</Card>
|
|
</Section>
|
|
|
|
<Section title="Details">
|
|
<Card elevated={false}>
|
|
<InfoRow label="Days supply" value={`${rx.daysSupply} days`} />
|
|
<InfoRow
|
|
label="Last filled"
|
|
value={
|
|
rx.lastFilledAt
|
|
? new Date(rx.lastFilledAt).toLocaleDateString()
|
|
: "Not filled yet"
|
|
}
|
|
/>
|
|
<InfoRow
|
|
label="Submitted"
|
|
value={new Date(rx.createdAt).toLocaleDateString()}
|
|
last={!rx.notes}
|
|
/>
|
|
{rx.notes ? <InfoRow label="Notes" value={rx.notes} last /> : null}
|
|
</Card>
|
|
</Section>
|
|
|
|
<Section title="Reminders">
|
|
{reminders.length === 0 ? (
|
|
<Card elevated={false}>
|
|
<Text variant="bodySm" color={colors.muted}>
|
|
No reminders yet. Set times so we can nudge you — and escalate if a dose is missed.
|
|
</Text>
|
|
</Card>
|
|
) : (
|
|
reminders.map((r) => (
|
|
<Card key={r.id} elevated={false} style={{ marginBottom: 8 }}>
|
|
<Text variant="h3">{r.medicineName}</Text>
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 4 }}>
|
|
{r.times.join(" · ")} · {r.graceMinutes} min grace
|
|
{r.escalateOnMiss ? " · escalate if missed" : ""}
|
|
</Text>
|
|
</Card>
|
|
))
|
|
)}
|
|
<Button
|
|
title="Manage calendar reminders"
|
|
variant="soft"
|
|
icon="calendar-outline"
|
|
onPress={() => router.push(routes.rxCalendar(rx.id))}
|
|
style={{ marginTop: 8 }}
|
|
/>
|
|
</Section>
|
|
|
|
<View style={{ gap: 10, marginTop: 8 }}>
|
|
<Button
|
|
title="Reorder medicines"
|
|
icon="bag-add-outline"
|
|
onPress={() => {
|
|
const match = products.find((p) =>
|
|
rx.medicineName.toLowerCase().includes(p.name.toLowerCase().split(" ")[0])
|
|
);
|
|
if (match) addProduct(match);
|
|
router.push(routes.cart);
|
|
}}
|
|
/>
|
|
<Button
|
|
title="Mark as refilled"
|
|
variant="outline"
|
|
onPress={() => markRefilled(rx.id)}
|
|
/>
|
|
{rx.status === "under_review" || rx.status === "submitted" ? (
|
|
<Button
|
|
title="Simulate pharmacist approval"
|
|
variant="soft"
|
|
onPress={() => updateRxStatus(rx.id, "approved")}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
</ScrollView>
|
|
</Screen>
|
|
);
|
|
}
|