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/calendar/[id].tsx
kirukib 7310a349c5 Add Gishen mobile app with complete demo shopping and care flows.
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>
2026-08-07 01:21:32 +03:00

266 lines
9.1 KiB
TypeScript

import { useLocalSearchParams } from "expo-router";
import { useMemo, useState } from "react";
import { Alert, 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, SwitchRow } from "@/components/ui/Field";
import { AppHeader } from "@/components/ui/Header";
import { EmptyState } from "@/components/ui/EmptyState";
import { Screen } from "@/components/ui/Screen";
import { Text } from "@/components/ui/Text";
import {
scheduleDoseReminder,
scheduleEscalationCheck,
} from "@/lib/notifications";
import { useEmergencyContactStore } from "@/lib/stores/emergencyContactStore";
import { usePrescriptionStore } from "@/lib/stores/prescriptionStore";
import { colors, gutter } from "@/lib/theme";
const PRESETS = ["08:00", "12:00", "14:00", "18:00", "20:00"] as const;
export default function RxCalendarScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const rx = usePrescriptionStore((s) => s.prescriptions.find((p) => p.id === id));
const upsertReminder = usePrescriptionStore((s) => s.upsertReminder);
const schedulePendingDose = usePrescriptionStore((s) => s.schedulePendingDose);
const confirmDose = usePrescriptionStore((s) => s.confirmDose);
const escalateDose = usePrescriptionStore((s) => s.escalateDose);
const doseLogs = usePrescriptionStore((s) => s.doseLogs);
const reminder = usePrescriptionStore((s) =>
s.reminders.find((r) => r.prescriptionId === id)
);
const grace = useEmergencyContactStore((s) => s.graceMinutesDefault);
const setGrace = useEmergencyContactStore((s) => s.setGrace);
const escalationEnabled = useEmergencyContactStore((s) => s.escalationEnabled);
const setEscalationEnabled = useEmergencyContactStore((s) => s.setEscalationEnabled);
const contacts = useEmergencyContactStore((s) => s.contacts);
const markEscalated = useEmergencyContactStore((s) => s.markEscalated);
const [times, setTimes] = useState(reminder?.times.join(",") || "08:00,20:00");
const [pendingLogId, setPendingLogId] = useState<string | null>(null);
const reminderId = `rem_${id}`;
const relatedLogs = useMemo(
() => doseLogs.filter((l) => l.reminderId === reminderId).slice(0, 8),
[doseLogs, reminderId]
);
if (!rx) {
return (
<Screen>
<EmptyState icon="calendar-outline" title="Prescription not found" />
</Screen>
);
}
const togglePreset = (t: string) => {
const list = times
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (list.includes(t)) {
setTimes(list.filter((x) => x !== t).join(","));
} else {
setTimes([...list, t].sort().join(","));
}
};
const saveAndSchedule = async () => {
const parsed = times
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (!parsed.length) {
Alert.alert("Times required", "Pick at least one reminder time.");
return;
}
upsertReminder({
id: reminderId,
prescriptionId: rx.id,
medicineName: rx.medicineName,
times: parsed,
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
graceMinutes: grace,
escalateOnMiss: escalationEnabled,
enabled: true,
});
await scheduleDoseReminder({
title: `Time for ${rx.medicineName}`,
body: "Tap to confirm you took your dose",
secondsFromNow: 15,
data: { prescriptionId: rx.id, type: "dose" },
});
const log = schedulePendingDose(reminderId);
setPendingLogId(log.id);
if (escalationEnabled && contacts.length) {
await scheduleEscalationCheck({
title: "Dose not confirmed",
body: `Notifying ${contacts[0].name} if still unconfirmed`,
graceSeconds: Math.max(20, grace * 60),
data: { logId: log.id, type: "escalation" },
});
setTimeout(() => {
const current = usePrescriptionStore
.getState()
.doseLogs.find((l) => l.id === log.id);
if (current?.status === "pending") {
escalateDose(log.id);
markEscalated();
Alert.alert("Emergency", `${contacts[0].name} would be notified (mock SMS/push)`);
}
}, Math.max(25000, grace * 1000));
} else if (escalationEnabled && !contacts.length) {
Alert.alert(
"No emergency contact",
"Add a contact under Profile → Emergency contacts for escalation."
);
}
Alert.alert("Scheduled", `Reminders at ${parsed.join(", ")}. Demo push in ~15s.`);
};
return (
<Screen>
<AppHeader title="Reminders" subtitle={rx.medicineName} />
<ScrollView
contentContainerStyle={{
paddingHorizontal: gutter,
paddingBottom: 40,
gap: 14,
}}
>
<Card>
<Text variant="caption" color={colors.muted}>
Patient
</Text>
<Text variant="h3" style={{ marginTop: 4 }}>
{rx.patientName || "Self"}
</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 4 }}>
{rx.status} · {rx.daysSupply} day supply
</Text>
</Card>
<Card>
<Text variant="h3">Dose times</Text>
<Text variant="caption" color={colors.muted} style={{ marginTop: 4 }}>
Tap presets or edit the comma-separated list
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, marginTop: 12 }}>
{PRESETS.map((p) => {
const selected = times
.split(",")
.map((s) => s.trim())
.includes(p);
return (
<Chip
key={p}
label={p}
selected={selected}
onPress={() => togglePreset(p)}
size="sm"
/>
);
})}
</View>
<View style={{ marginTop: 12 }}>
<Field
label="Times (HH:mm)"
value={times}
onChangeText={setTimes}
hint="Example: 08:00,14:00,20:00"
/>
</View>
</Card>
<Card padded={false}>
<SwitchRow
icon="alert-circle-outline"
title="Escalate if missed"
subtitle={
contacts[0]
? `Notify ${contacts[0].name} after grace period`
: "Add an emergency contact first"
}
value={escalationEnabled}
onValueChange={setEscalationEnabled}
/>
<View style={{ padding: 16, paddingTop: 4 }}>
<Field
label="Grace minutes"
value={String(grace)}
keyboardType="number-pad"
onChangeText={(v) => setGrace(Number(v) || 30)}
/>
</View>
</Card>
{pendingLogId ? (
<Card tone="brand" elevated={false}>
<Text variant="h3">Confirm this dose</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 4 }}>
Demo reminder is active. Mark taken or snooze.
</Text>
<View style={{ flexDirection: "row", gap: 10, marginTop: 14 }}>
<Button
title="Taken"
style={{ flex: 1 }}
onPress={() => {
confirmDose(pendingLogId);
setPendingLogId(null);
Alert.alert("Logged", "Dose confirmed");
}}
/>
<Button
title="Snooze 10m"
variant="outline"
style={{ flex: 1 }}
onPress={() =>
scheduleDoseReminder({
title: `Snoozed: ${rx.medicineName}`,
body: "Reminder",
secondsFromNow: 600,
})
}
/>
</View>
</Card>
) : null}
<Button title="Save & schedule push" onPress={saveAndSchedule} />
<Text variant="h3">Recent dose logs</Text>
{relatedLogs.length === 0 && doseLogs.length === 0 ? (
<Text variant="bodySm" color={colors.muted}>
No logs yet. Schedule a reminder to start tracking.
</Text>
) : (
(relatedLogs.length ? relatedLogs : doseLogs.slice(0, 8)).map((l) => (
<Card key={l.id} elevated={false}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<Badge
label={l.status.replace("_", " ")}
tone={
l.status === "taken"
? "brand"
: l.status === "missed_escalated"
? "danger"
: l.status === "pending"
? "warn"
: "neutral"
}
/>
<Text variant="bodySm" color={colors.muted} style={{ flex: 1 }}>
{new Date(l.scheduledAt).toLocaleString()}
</Text>
</View>
</Card>
))
)}
</ScrollView>
</Screen>
);
}