This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Mob/app/(root)/(screens)/prescription/[id].tsx
kirukib d40cb22ff4 Finalize profile, prescription, and checkout with shared UI modules.
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>
2026-08-10 14:53:34 +03:00

434 lines
16 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import { useMemo, useState } from "react";
import { Alert, Image, ScrollView, View } from "react-native";
import {
RxImagePager,
RxMedicineLines,
RxQueriedPanel,
RxStatusStepper,
} from "@/components/prescription";
import { EscalationBanner } from "@/components/care";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { Card, InfoRow, Section } from "@/components/ui/Card";
import { DoseReminderPanel } from "@/components/ui/DoseReminderPanel";
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 { scheduleDoseReminder } from "@/lib/notifications";
import { routes } from "@/lib/routes";
import { useCartStore } from "@/lib/stores/cartStore";
import { useNotificationStore } from "@/lib/stores/notificationStore";
import { usePrescriptionStore } from "@/lib/stores/prescriptionStore";
import { colors, gutter } from "@/lib/theme";
const statusTone = {
submitted: "neutral",
under_review: "warn",
approved: "brand",
queried: "info",
rejected: "danger",
filled: "brand",
} as const;
export default function PrescriptionDetailScreen() {
const params = useLocalSearchParams<{ id: string }>();
const id = Array.isArray(params.id) ? params.id[0] : params.id;
const rx = usePrescriptionStore((s) => s.prescriptions.find((p) => p.id === id));
const allReminders = usePrescriptionStore((s) => s.reminders);
const doseLogs = usePrescriptionStore((s) => s.doseLogs);
const reminders = useMemo(
() => allReminders.filter((r) => r.prescriptionId === id),
[allReminders, id]
);
const reminder = reminders[0];
const reminderId = `rem_${id}`;
const recentLogs = useMemo(
() => doseLogs.filter((l) => l.reminderId === reminderId).slice(0, 5),
[doseLogs, reminderId]
);
const markRefilled = usePrescriptionStore((s) => s.markRefilled);
const updateRxStatus = usePrescriptionStore((s) => s.updateRxStatus);
const replyToQuery = usePrescriptionStore((s) => s.replyToQuery);
const confirmDose = usePrescriptionStore((s) => s.confirmDose);
const addProduct = useCartStore((s) => s.addProduct);
const pushNotification = useNotificationStore((s) => s.push);
const [showSetup, setShowSetup] = useState(!reminder?.enabled);
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));
})();
const pendingLog = recentLogs.find((l) => l.status === "pending");
const refillSoon = dueIn !== null && dueIn <= 5;
return (
<Screen>
<AppHeader
title="Prescription"
subtitle={rx.patientName}
right={<Badge label={rx.status.replace("_", " ")} tone={statusTone[rx.status]} />}
/>
<ScrollView
contentContainerStyle={{
paddingHorizontal: gutter,
paddingBottom: 40,
gap: 14,
}}
>
<Card elevated={false}>
<Text variant="label" color={colors.muted} style={{ marginBottom: 12 }}>
Pharmacist review status
</Text>
<RxStatusStepper status={rx.status} />
</Card>
{rx.status === "queried" ? (
<RxQueriedPanel
pharmacistMessage={rx.pharmacistMessage}
customerReply={rx.customerReply}
onReply={(message) => {
replyToQuery(rx.id, message);
pushNotification({
kind: "rx",
title: "Reply sent",
body: "Your clarification was sent back to the pharmacist.",
href: routes.rxDetail(rx.id),
});
Alert.alert("Sent", "Status moved back to under review.");
}}
/>
) : null}
{rx.status === "rejected" ? (
<Card tone="wash" elevated={false} style={{ borderColor: colors.danger }}>
<Text variant="h3" color={colors.danger}>
Prescription rejected
</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 6 }}>
{rx.pharmacistMessage ||
"The pharmacist could not approve this script. Upload a clearer copy or visit a branch."}
</Text>
<Button
title="Scan new prescription"
style={{ marginTop: 12 }}
onPress={() => router.push(routes.rxScan)}
/>
</Card>
) : null}
{refillSoon ? (
<Card tone="brand" elevated={false}>
<Text variant="h3">
{dueIn !== null && dueIn <= 0 ? "Refill overdue" : "Refill reminder"}
</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 6 }}>
{dueIn !== null && dueIn <= 0
? "Days of supply have run out — reorder or mark refilled after pickup."
: `About ${dueIn} day${dueIn === 1 ? "" : "s"} of supply left.`}
</Text>
<Button
title="One-tap refill to cart"
style={{ marginTop: 12 }}
icon="bag-add-outline"
onPress={() => {
rx.medicines.forEach((m) => {
const match = products.find((p) =>
p.name.toLowerCase().includes(m.name.toLowerCase().split(" ")[0])
);
if (match) addProduct(match);
});
router.push(routes.cart);
}}
/>
</Card>
) : null}
<Card elevated style={{ padding: 0, overflow: "hidden" }}>
<View
style={{
height: 160,
backgroundColor: colors.brandSoft,
alignItems: "center",
justifyContent: "center",
}}
>
{rx.imageUris[0] ? (
<Image
source={{ uri: rx.imageUris[0] }}
style={{ width: "100%", height: "100%" }}
resizeMode="cover"
/>
) : (
<Ionicons name="document-text" size={48} color={colors.brandText} />
)}
</View>
<View style={{ padding: 16, gap: 10 }}>
<Text variant="h1">{rx.medicineName}</Text>
<Text variant="bodySm" color={colors.muted}>
For {rx.patientName} · {rx.medicines.length} item
{rx.medicines.length === 1 ? "" : "s"} on this Rx
</Text>
{rx.imageUris.length > 1 ? (
<View style={{ marginTop: 4 }}>
<Text variant="caption" color={colors.subtle} style={{ marginBottom: 8 }}>
Pages
</Text>
<RxImagePager uris={rx.imageUris} size={72} />
</View>
) : null}
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
<Badge label={`${rx.daysSupply} day supply`} tone="neutral" icon="time-outline" />
{dueIn !== null ? (
<Badge
label={dueIn <= 0 ? "Refill overdue" : `Refill in ${dueIn} days`}
tone={dueIn <= 3 ? "warn" : "brand"}
/>
) : (
<Badge label="Awaiting first fill" tone="warn" />
)}
{reminder?.enabled ? (
<Badge label={reminder.times.join(" · ")} tone="brand" icon="alarm-outline" />
) : (
<Badge label="Reminders off" tone="neutral" icon="alarm-outline" />
)}
</View>
</View>
</Card>
<Section title="Dose calendar & push">
{reminder?.escalateOnMiss ? (
<View style={{ marginBottom: 10 }}>
<EscalationBanner />
</View>
) : null}
<Card>
{reminder?.enabled && !showSetup ? (
<View style={{ gap: 12 }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<View
style={{
width: 44,
height: 44,
borderRadius: 14,
backgroundColor: colors.brandSoft,
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons name="notifications" size={20} color={colors.brandText} />
</View>
<View style={{ flex: 1 }}>
<Text variant="h3">Daily reminders on</Text>
<Text variant="caption" color={colors.muted} style={{ marginTop: 2 }}>
Push at {reminder.times.join(" · ")}
{reminder.escalateOnMiss ? " · escalate if missed" : ""}
</Text>
</View>
</View>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
<Button
title="Edit times"
size="md"
fullWidth={false}
variant="outline"
onPress={() => setShowSetup(true)}
/>
<Button
title="Open calendar"
size="md"
fullWidth={false}
icon="calendar-outline"
onPress={() => router.push(routes.rxCalendar(rx.id))}
/>
<Button
title="Test push now"
size="md"
fullWidth={false}
variant="soft"
icon="flash-outline"
onPress={async () => {
await scheduleDoseReminder({
title: `Time for ${rx.medicineName}`,
body: "Test dose reminder from Gishen",
secondsFromNow: 5,
data: { prescriptionId: rx.id, type: "dose" },
});
pushNotification({
kind: "dose",
title: "Test dose reminder",
body: `${rx.medicineName} push scheduled in ~5s`,
href: routes.rxDetail(rx.id),
});
Alert.alert("Scheduled", "Test push in about 5 seconds.");
}}
/>
</View>
</View>
) : (
<DoseReminderPanel
rx={rx}
reminder={reminder}
onScheduled={() => setShowSetup(false)}
/>
)}
</Card>
{pendingLog ? (
<Card tone="brand" elevated={false} style={{ marginTop: 10 }}>
<Text variant="h3">Confirm today’s dose</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 4 }}>
A dose is pending confirmation.
</Text>
<View style={{ flexDirection: "row", gap: 10, marginTop: 12 }}>
<Button
title="Taken"
style={{ flex: 1 }}
onPress={() => {
confirmDose(pendingLog.id);
Alert.alert("Logged", "Dose marked as taken.");
}}
/>
<Button
title="Snooze 10m"
variant="outline"
style={{ flex: 1 }}
onPress={() =>
scheduleDoseReminder({
title: `Snoozed: ${rx.medicineName}`,
body: "Take your dose when ready",
secondsFromNow: 600,
data: { prescriptionId: rx.id, type: "dose" },
})
}
/>
</View>
</Card>
) : null}
{recentLogs.length ? (
<View style={{ marginTop: 12, gap: 8 }}>
<Text variant="label" color={colors.muted}>
Recent dose logs
</Text>
{recentLogs.map((l) => (
<View
key={l.id}
style={{
flexDirection: "row",
alignItems: "center",
gap: 10,
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: colors.line,
}}
>
<Badge
label={l.status.replace("_", " ")}
tone={
l.status === "taken"
? "brand"
: l.status === "missed_escalated"
? "danger"
: "warn"
}
/>
<Text variant="caption" color={colors.subtle} style={{ flex: 1 }}>
{new Date(l.scheduledAt).toLocaleString()}
</Text>
</View>
))}
</View>
) : null}
</Section>
<Section title="Medicines on this Rx">
<RxMedicineLines medicines={rx.medicines} />
</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>
<View style={{ gap: 10 }}>
<Button
title="Reorder medicines"
icon="bag-add-outline"
onPress={() => {
rx.medicines.forEach((m) => {
const match = products.find((p) =>
p.name.toLowerCase().includes(m.name.toLowerCase().split(" ")[0])
);
if (match) addProduct(match);
});
router.push(routes.cart);
}}
/>
<Button
title="Mark as refilled"
variant="outline"
onPress={() => {
markRefilled(rx.id);
Alert.alert("Updated", "Marked as refilled today.");
}}
/>
{rx.status === "under_review" || rx.status === "submitted" ? (
<Button
title="Simulate pharmacist approval"
variant="soft"
onPress={() => updateRxStatus(rx.id, "approved")}
/>
) : null}
{rx.status === "submitted" ? (
<Button
title="Simulate pharmacist query"
variant="ghost"
onPress={() =>
updateRxStatus(
rx.id,
"queried",
"Please confirm the prescribed strength on page 1."
)
}
/>
) : null}
</View>
</ScrollView>
</Screen>
);
}