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>
254 lines
8.8 KiB
TypeScript
254 lines
8.8 KiB
TypeScript
import { router } from "expo-router";
|
|
import { useMemo, useState } from "react";
|
|
import { Alert, ScrollView, View } from "react-native";
|
|
import { RxImagePager, RxStatusStepper } from "@/components/prescription";
|
|
import { Badge } from "@/components/ui/Badge";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card } from "@/components/ui/Card";
|
|
import { Chip } from "@/components/ui/Chip";
|
|
import { Field } from "@/components/ui/Field";
|
|
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 { routes } from "@/lib/routes";
|
|
import { useAuthStore } from "@/lib/stores/authStore";
|
|
import { useCartStore } from "@/lib/stores/cartStore";
|
|
import { useFamilyStore } from "@/lib/stores/familyStore";
|
|
import { useLocationStore } from "@/lib/stores/locationStore";
|
|
import { useNotificationStore } from "@/lib/stores/notificationStore";
|
|
import { usePrescriptionStore } from "@/lib/stores/prescriptionStore";
|
|
import { colors, gutter } from "@/lib/theme";
|
|
|
|
function matchStatus(name: string): "ready" | "review" | "unavailable" {
|
|
const hit = products.find((p) =>
|
|
p.name.toLowerCase().includes(name.trim().toLowerCase())
|
|
);
|
|
if (!hit) return "review";
|
|
if (!hit.inStock) return "unavailable";
|
|
return "ready";
|
|
}
|
|
|
|
export default function RxReviewScreen() {
|
|
const draftImages = usePrescriptionStore((s) => s.draftImages);
|
|
const setDraftImages = usePrescriptionStore((s) => s.setDraftImages);
|
|
const addPrescription = usePrescriptionStore((s) => s.addPrescription);
|
|
const addProduct = useCartStore((s) => s.addProduct);
|
|
const setFulfillment = useLocationStore((s) => s.setFulfillment);
|
|
const pushNotification = useNotificationStore((s) => s.push);
|
|
const user = useAuthStore((s) => s.user);
|
|
const members = useFamilyStore((s) => s.members);
|
|
const activeMemberId = useFamilyStore((s) => s.activeMemberId);
|
|
const activeMember = members.find((m) => m.id === activeMemberId);
|
|
|
|
const [patientName, setPatientName] = useState(
|
|
activeMember?.name || user?.name || ""
|
|
);
|
|
const [memberId, setMemberId] = useState<string | null>(activeMemberId);
|
|
const [medicineName, setMedicineName] = useState("Metformin 500mg");
|
|
const [daysSupply, setDaysSupply] = useState("30");
|
|
const [list, setList] = useState("Metformin 500mg, Vitamin B12");
|
|
const [notes, setNotes] = useState("");
|
|
|
|
const medicines = useMemo(() => {
|
|
return list
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.map((name) => ({ name, status: matchStatus(name) }));
|
|
}, [list]);
|
|
|
|
const save = (toCart: boolean) => {
|
|
if (!draftImages.length) {
|
|
Alert.alert("Image required", "Scan or upload at least one prescription photo first.");
|
|
return;
|
|
}
|
|
if (!patientName.trim() || !medicineName.trim()) {
|
|
Alert.alert("Missing", "Patient and primary medicine are required.");
|
|
return;
|
|
}
|
|
const meds =
|
|
medicines.length > 0
|
|
? medicines
|
|
: [{ name: medicineName, status: matchStatus(medicineName) }];
|
|
const rx = addPrescription({
|
|
patientName: patientName.trim(),
|
|
memberId,
|
|
medicineName: medicineName.trim(),
|
|
medicines: meds,
|
|
imageUris: draftImages,
|
|
daysSupply: Number(daysSupply) || 30,
|
|
lastFilledAt: null,
|
|
notes: notes.trim(),
|
|
});
|
|
if (toCart) {
|
|
meds
|
|
.filter((m) => m.status === "ready")
|
|
.forEach((m) => {
|
|
const p = products.find((x) =>
|
|
x.name.toLowerCase().includes(m.name.toLowerCase().split(" ")[0])
|
|
);
|
|
if (p) addProduct(p);
|
|
});
|
|
setFulfillment("pickup");
|
|
}
|
|
setDraftImages([]);
|
|
pushNotification({
|
|
kind: "rx",
|
|
title: "Prescription submitted",
|
|
body: "A pharmacist will review your script shortly.",
|
|
href: routes.rxDetail(rx.id),
|
|
});
|
|
Alert.alert("Submitted", "Your prescription is under review.", [
|
|
{ text: "Track status", onPress: () => router.replace(routes.rxDetail(rx.id)) },
|
|
{ text: "Reminders", onPress: () => router.replace(routes.rxCalendar(rx.id)) },
|
|
...(toCart
|
|
? [{ text: "Cart", onPress: () => router.replace(routes.cart) }]
|
|
: [{ text: "List", onPress: () => router.replace(routes.rxList) }]),
|
|
]);
|
|
};
|
|
|
|
return (
|
|
<Screen>
|
|
<AppHeader title="Review prescription" subtitle="Confirm patient and medicines" />
|
|
<ScrollView
|
|
contentContainerStyle={{ paddingHorizontal: gutter, paddingBottom: 40, gap: 14 }}
|
|
>
|
|
<Card elevated={false}>
|
|
<Text variant="label" color={colors.muted} style={{ marginBottom: 12 }}>
|
|
After submit
|
|
</Text>
|
|
<RxStatusStepper status="submitted" />
|
|
</Card>
|
|
|
|
<Card elevated={false}>
|
|
<Text variant="h3" style={{ marginBottom: 10 }}>
|
|
Prescription pages ({draftImages.length})
|
|
</Text>
|
|
{draftImages.length ? (
|
|
<RxImagePager
|
|
uris={draftImages}
|
|
size={104}
|
|
onLongPress={(_, index) => {
|
|
Alert.alert("Remove page?", `Page ${index + 1}`, [
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Remove",
|
|
style: "destructive",
|
|
onPress: () =>
|
|
setDraftImages(draftImages.filter((_, i) => i !== index)),
|
|
},
|
|
]);
|
|
}}
|
|
/>
|
|
) : (
|
|
<>
|
|
<Text variant="bodySm" color={colors.muted}>
|
|
Go back to scan or upload a prescription photo.
|
|
</Text>
|
|
<Button
|
|
title="Scan / upload"
|
|
size="md"
|
|
style={{ marginTop: 12 }}
|
|
onPress={() => router.replace(routes.rxScan)}
|
|
/>
|
|
</>
|
|
)}
|
|
</Card>
|
|
|
|
<Card elevated={false}>
|
|
<Text variant="h3" style={{ marginBottom: 10 }}>
|
|
Patient
|
|
</Text>
|
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, marginBottom: 12 }}>
|
|
<Chip
|
|
label={user?.name || "Me"}
|
|
selected={!memberId}
|
|
onPress={() => {
|
|
setMemberId(null);
|
|
setPatientName(user?.name || "");
|
|
}}
|
|
/>
|
|
{members
|
|
.filter((m) => m.status === "accepted")
|
|
.map((m) => (
|
|
<Chip
|
|
key={m.id}
|
|
label={m.name}
|
|
selected={memberId === m.id}
|
|
onPress={() => {
|
|
setMemberId(m.id);
|
|
setPatientName(m.name);
|
|
}}
|
|
/>
|
|
))}
|
|
</View>
|
|
<View style={{ gap: 12 }}>
|
|
<Field label="Patient name" value={patientName} onChangeText={setPatientName} />
|
|
<Field
|
|
label="Primary medicine"
|
|
value={medicineName}
|
|
onChangeText={setMedicineName}
|
|
hint="Used as the prescription title"
|
|
/>
|
|
<Field
|
|
label="All medicines (comma-separated)"
|
|
value={list}
|
|
onChangeText={setList}
|
|
placeholder="Metformin 500mg, Vitamin B12"
|
|
/>
|
|
<Field
|
|
label="Days supply"
|
|
value={daysSupply}
|
|
onChangeText={setDaysSupply}
|
|
keyboardType="number-pad"
|
|
/>
|
|
<Field
|
|
label="Notes for pharmacist"
|
|
value={notes}
|
|
onChangeText={setNotes}
|
|
placeholder="Allergies, instructions, preferred brand…"
|
|
/>
|
|
</View>
|
|
</Card>
|
|
|
|
<Card elevated={false}>
|
|
<Text variant="h3" style={{ marginBottom: 10 }}>
|
|
Availability preview
|
|
</Text>
|
|
{(medicines.length
|
|
? medicines
|
|
: [{ name: medicineName || "—", status: "review" as const }]
|
|
).map((m) => (
|
|
<View
|
|
key={m.name}
|
|
style={{
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
paddingVertical: 8,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.line,
|
|
}}
|
|
>
|
|
<Text variant="bodyMedium">{m.name}</Text>
|
|
<Badge
|
|
label={m.status}
|
|
tone={
|
|
m.status === "ready" ? "brand" : m.status === "review" ? "warn" : "danger"
|
|
}
|
|
/>
|
|
</View>
|
|
))}
|
|
</Card>
|
|
|
|
<Button title="Submit for review" onPress={() => save(false)} />
|
|
<Button
|
|
title="Submit & add ready items to cart"
|
|
variant="soft"
|
|
onPress={() => save(true)}
|
|
/>
|
|
</ScrollView>
|
|
</Screen>
|
|
);
|
|
}
|