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>
223 lines
7.9 KiB
TypeScript
223 lines
7.9 KiB
TypeScript
import { router } from "expo-router";
|
|
import { useMemo, useState } from "react";
|
|
import { Alert, Image, ScrollView, View } from "react-native";
|
|
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 { usePrescriptionStore } from "@/lib/stores/prescriptionStore";
|
|
import { colors, gutter, radii } 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 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([]);
|
|
Alert.alert("Saved", "Prescription submitted for pharmacist review", [
|
|
{ text: "Open details", onPress: () => router.replace(routes.rxDetail(rx.id)) },
|
|
{ text: "Reminders", onPress: () => router.replace(routes.rxCalendar(rx.id)) },
|
|
{ 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 }}>
|
|
{draftImages.length ? (
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8 }}>
|
|
{draftImages.map((uri) => (
|
|
<Image
|
|
key={uri}
|
|
source={{ uri }}
|
|
style={{ width: 96, height: 96, borderRadius: radii.md, backgroundColor: colors.surfaceAlt }}
|
|
/>
|
|
))}
|
|
</ScrollView>
|
|
) : (
|
|
<Card tone="brand" elevated={false}>
|
|
<Text variant="h3">No images attached</Text>
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 4 }}>
|
|
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>
|
|
);
|
|
}
|