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)/(tabs)/prescriptions.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

399 lines
14 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { useMemo, useState } from "react";
import { Alert, Image, Pressable, Share, ScrollView, View } from "react-native";
import { useTranslation } from "react-i18next";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { Card, Section } from "@/components/ui/Card";
import { DoseReminderPanel } from "@/components/ui/DoseReminderPanel";
import { EmptyState } from "@/components/ui/EmptyState";
import { Screen, TAB_BAR_INSET } from "@/components/ui/Screen";
import { TabTopBar } from "@/components/ui/TabTopBar";
import { Text } from "@/components/ui/Text";
import { routes } from "@/lib/routes";
import { useFamilyStore } from "@/lib/stores/familyStore";
import {
usePrescriptionStore,
type Prescription,
type Reminder,
} 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;
function refillDays(rx: Prescription) {
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));
}
function RxCareCard({
rx,
reminder,
expanded,
onToggleExpand,
}: {
rx: Prescription;
reminder?: Reminder;
expanded: boolean;
onToggleExpand: () => void;
}) {
const dueIn = refillDays(rx);
const thumb = rx.imageUris[0];
return (
<Card elevated style={{ padding: 0, overflow: "hidden" }}>
<Pressable
onPress={() => router.push(routes.rxDetail(rx.id))}
style={{ padding: 14 }}
>
<View style={{ flexDirection: "row", gap: 12 }}>
<View
style={{
width: 64,
height: 64,
borderRadius: 16,
backgroundColor: colors.brandSoft,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
}}
>
{thumb ? (
<Image source={{ uri: thumb }} style={{ width: 64, height: 64 }} />
) : (
<Ionicons name="medkit" size={26} color={colors.brandText} />
)}
</View>
<View style={{ flex: 1 }}>
<View style={{ flexDirection: "row", gap: 8, alignItems: "flex-start" }}>
<Text variant="h3" style={{ flex: 1 }} numberOfLines={2}>
{rx.medicineName}
</Text>
<Badge label={rx.status.replace("_", " ")} tone={statusTone[rx.status]} />
</View>
<Text variant="caption" color={colors.subtle} style={{ marginTop: 4 }}>
{rx.patientName} · {rx.medicines.length} medicine
{rx.medicines.length === 1 ? "" : "s"} · {rx.daysSupply}d supply
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 10 }}>
{reminder?.enabled ? (
<Badge
label={`Reminders ${reminder.times.join(" · ")}`}
tone="brand"
icon="alarm-outline"
/>
) : (
<Badge label="No dose reminder" tone="warn" icon="alarm-outline" />
)}
{dueIn !== null ? (
<Badge
label={
dueIn <= 0 ? "Refill overdue" : `Refill in ${dueIn}d`
}
tone={dueIn <= 3 ? "warn" : "neutral"}
/>
) : (
<Badge label="Awaiting fill" tone="info" />
)}
</View>
</View>
</View>
</Pressable>
<View
style={{
flexDirection: "row",
borderTopWidth: 1,
borderTopColor: colors.line,
}}
>
<Pressable
onPress={onToggleExpand}
style={{
flex: 1,
paddingVertical: 12,
alignItems: "center",
flexDirection: "row",
justifyContent: "center",
gap: 6,
}}
>
<Ionicons
name={expanded ? "chevron-up" : "notifications-outline"}
size={16}
color={colors.brandText}
/>
<Text variant="label" color={colors.brandText}>
{expanded ? "Hide setup" : reminder?.enabled ? "Edit reminder" : "Set reminder"}
</Text>
</Pressable>
<View style={{ width: 1, backgroundColor: colors.line }} />
<Pressable
onPress={() => router.push(routes.rxCalendar(rx.id))}
style={{
flex: 1,
paddingVertical: 12,
alignItems: "center",
flexDirection: "row",
justifyContent: "center",
gap: 6,
}}
>
<Ionicons name="calendar-outline" size={16} color={colors.brandText} />
<Text variant="label" color={colors.brandText}>
Calendar
</Text>
</Pressable>
</View>
{expanded ? (
<View style={{ paddingHorizontal: 14, paddingBottom: 14 }}>
<DoseReminderPanel rx={rx} reminder={reminder} compact />
</View>
) : null}
</Card>
);
}
export default function PrescriptionsTabScreen() {
const { t } = useTranslation();
const prescriptions = usePrescriptionStore((s) => s.prescriptions);
const reminders = usePrescriptionStore((s) => s.reminders);
const members = useFamilyStore((s) => s.members);
const inviteCode = useFamilyStore((s) => s.inviteCode);
const acceptInvite = useFamilyStore((s) => s.acceptInvite);
const removeMember = useFamilyStore((s) => s.removeMember);
const setActiveMember = useFamilyStore((s) => s.setActiveMember);
const activeMemberId = useFamilyStore((s) => s.activeMemberId);
const [expandedId, setExpandedId] = useState<string | null>(null);
const reminderByRx = useMemo(() => {
const map = new Map<string, Reminder>();
reminders.forEach((r) => map.set(r.prescriptionId, r));
return map;
}, [reminders]);
const activeReminderCount = reminders.filter((r) => r.enabled).length;
const needingSetup = prescriptions.filter((p) => !reminderByRx.get(p.id)?.enabled).length;
return (
<Screen>
<TabTopBar subtitle={t("common.prescriptions")} />
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingBottom: TAB_BAR_INSET + 24,
paddingHorizontal: gutter,
gap: 14,
}}
>
<View>
<Text variant="h1">{t("common.prescriptions")}</Text>
<Text variant="bodySm" color={colors.subtle} style={{ marginTop: 4 }}>
Manage Rx, dose calendars, and family care shares
</Text>
</View>
<View style={{ flexDirection: "row", gap: 10 }}>
<Card elevated={false} tone="brand" style={{ flex: 1 }}>
<Text variant="caption" color={colors.muted}>
Active reminders
</Text>
<Text variant="h1" style={{ marginTop: 2 }}>
{activeReminderCount}
</Text>
</Card>
<Card elevated={false} style={{ flex: 1 }}>
<Text variant="caption" color={colors.muted}>
Need setup
</Text>
<Text variant="h1" style={{ marginTop: 2 }}>
{needingSetup}
</Text>
</Card>
</View>
<Button
title={t("common.scanRx")}
icon="scan-outline"
onPress={() => router.push(routes.rxScan)}
/>
<Section title="Your prescriptions">
{prescriptions.length === 0 ? (
<EmptyState
compact
icon="document-text-outline"
title="No prescriptions yet"
body="Scan or upload a prescription to start reminders."
actionTitle="Scan prescription"
onAction={() => router.push(routes.rxScan)}
/>
) : (
<View style={{ gap: 12 }}>
{prescriptions.map((rx) => (
<RxCareCard
key={rx.id}
rx={rx}
reminder={reminderByRx.get(rx.id)}
expanded={expandedId === rx.id}
onToggleExpand={() =>
setExpandedId((cur) => (cur === rx.id ? null : rx.id))
}
/>
))}
</View>
)}
</Section>
<Section title={t("common.family")}>
<Card tone="brand" elevated={false}>
<View style={{ flexDirection: "row", gap: 12, alignItems: "center" }}>
<View
style={{
width: 48,
height: 48,
borderRadius: 14,
backgroundColor: colors.surface,
alignItems: "center",
justifyContent: "center",
borderWidth: 1,
borderColor: colors.brandBorder,
}}
>
<Ionicons name="people" size={22} color={colors.brandText} />
</View>
<View style={{ flex: 1 }}>
<Text variant="caption" color={colors.muted}>
Family invite code
</Text>
<Text variant="h2" style={{ marginTop: 2, letterSpacing: 1 }}>
{inviteCode}
</Text>
</View>
</View>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 10 }}>
Share care so family can help with dose reminders and refills.
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, marginTop: 12 }}>
<Button
title="Share code"
size="md"
fullWidth={false}
variant="outline"
icon="share-outline"
onPress={() =>
Share.share({
message: `Join my Gishen family care group with code ${inviteCode}`,
})
}
/>
<Button
title={t("common.inviteFamily")}
size="md"
fullWidth={false}
icon="person-add-outline"
onPress={() => router.push(routes.familyInvite)}
/>
</View>
</Card>
{members.length === 0 ? (
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 8 }}>
No family members yet. Invite someone to share Rx care.
</Text>
) : (
<View style={{ gap: 10, marginTop: 4 }}>
{members.map((m) => {
const active = activeMemberId === m.id;
return (
<Card
key={m.id}
onPress={() => setActiveMember(active ? null : m.id)}
style={{
borderColor: active ? colors.brand : colors.line,
borderWidth: active ? 1.5 : 1,
}}
>
<View style={{ flexDirection: "row", gap: 12, alignItems: "center" }}>
<View
style={{
width: 42,
height: 42,
borderRadius: 21,
backgroundColor: colors.brandSoft,
alignItems: "center",
justifyContent: "center",
}}
>
<Text variant="label" color={colors.brandText}>
{m.name.slice(0, 1).toUpperCase()}
</Text>
</View>
<View style={{ flex: 1 }}>
<Text variant="h3">{m.name}</Text>
<Text variant="caption" color={colors.subtle} style={{ marginTop: 2 }}>
{m.relationship}
{m.phone ? ` · ${m.phone}` : ""}
{active ? " · Active for Rx" : ""}
</Text>
</View>
<Badge
label={m.status}
tone={m.status === "accepted" ? "brand" : "warn"}
/>
</View>
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginTop: 12,
}}
>
{m.status === "pending" ? (
<Button
title="Accept"
size="sm"
fullWidth={false}
onPress={() => acceptInvite(m.id)}
/>
) : null}
<Button
title="Remove"
size="sm"
variant="ghost"
fullWidth={false}
onPress={() =>
Alert.alert("Remove member?", m.name, [
{ text: "Cancel", style: "cancel" },
{
text: "Remove",
style: "destructive",
onPress: () => removeMember(m.id),
},
])
}
/>
</View>
</Card>
);
})}
</View>
)}
</Section>
</ScrollView>
</Screen>
);
}