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 5391780b9b Ship Rx Care tab, notifications inbox, and guest pickup codes.
Replace the profile tab with prescription management (inline reminders and family invite), add a shared tab top bar, separate notifications from orders, and support any-branch guest checkout with GSH codes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:35:51 +03:00

377 lines
13 KiB
TypeScript
Raw 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 { 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 { useCartStore } from "@/lib/stores/cartStore";
import { useNotificationStore } from "@/lib/stores/notificationStore";
import { usePrescriptionStore } from "@/lib/stores/prescriptionStore";
import { colors, gutter } 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 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 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");
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 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>
<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">
<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">
<Card elevated={false} padded={false}>
{rx.medicines.map((m, i) => (
<View
key={m.name}
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
paddingVertical: 14,
borderBottomWidth: i === rx.medicines.length - 1 ? 0 : 1,
borderBottomColor: colors.line,
}}
>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10, flex: 1 }}>
<View
style={{
width: 36,
height: 36,
borderRadius: 12,
backgroundColor: colors.brandSoft,
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons name="medkit-outline" size={16} color={colors.brandText} />
</View>
<Text variant="h3" style={{ flex: 1 }}>
{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>
<View style={{ gap: 10 }}>
<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);
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}
</View>
</ScrollView>
</Screen>
);
}