Guests can generate a shareable GSH code for admin fulfillment; sign-in now supports Google, email, phone, and Telegram demos, with the backend spec updated. Co-authored-by: Cursor <cursoragent@cursor.com>
276 lines
8.3 KiB
TypeScript
276 lines
8.3 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
||
import * as Clipboard from "expo-clipboard";
|
||
import { router, useLocalSearchParams } from "expo-router";
|
||
import { useState } from "react";
|
||
import {
|
||
Alert,
|
||
Image,
|
||
Linking,
|
||
Pressable,
|
||
ScrollView,
|
||
Share,
|
||
View,
|
||
} from "react-native";
|
||
import { Button } from "@/components/ui/Button";
|
||
import { AppHeader } from "@/components/ui/Header";
|
||
import { Screen } from "@/components/ui/Screen";
|
||
import { Text } from "@/components/ui/Text";
|
||
import { buildPharmacistOrderShareText } from "@/lib/commerce";
|
||
import { routes } from "@/lib/routes";
|
||
import { useOrdersStore } from "@/lib/stores/ordersStore";
|
||
import { colors, fonts, gutter, radii, shadows } from "@/lib/theme";
|
||
import { formatETB, WHATSAPP_NUMBER } from "@/lib/utils";
|
||
|
||
/**
|
||
* Screenshot-friendly pharmacist order code card.
|
||
* Guests copy / share / screenshot this so staff can create the order in admin.
|
||
*/
|
||
export default function PharmacistOrderCodeScreen() {
|
||
const { id } = useLocalSearchParams<{ id: string }>();
|
||
const order = useOrdersStore((s) => s.getById(id));
|
||
const [copied, setCopied] = useState(false);
|
||
|
||
if (!order?.pickupCode) {
|
||
return (
|
||
<Screen>
|
||
<AppHeader title="Order code" />
|
||
<View style={{ padding: gutter }}>
|
||
<Text variant="body" color={colors.muted}>
|
||
No pharmacist order code found for this order.
|
||
</Text>
|
||
<Button
|
||
title="Back to orders"
|
||
style={{ marginTop: 16 }}
|
||
onPress={() => router.replace(routes.orders)}
|
||
/>
|
||
</View>
|
||
</Screen>
|
||
);
|
||
}
|
||
|
||
const shareText = buildPharmacistOrderShareText(order);
|
||
const due = order.selfPay ?? order.total;
|
||
|
||
const copyCode = async () => {
|
||
await Clipboard.setStringAsync(order.pickupCode!);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 2000);
|
||
};
|
||
|
||
const copyFull = async () => {
|
||
await Clipboard.setStringAsync(shareText);
|
||
Alert.alert("Copied", "Order details copied — paste into chat or admin notes.");
|
||
};
|
||
|
||
const share = async () => {
|
||
try {
|
||
await Share.share({ message: shareText, title: `Gishen code ${order.pickupCode}` });
|
||
} catch {
|
||
// user dismissed
|
||
}
|
||
};
|
||
|
||
const whatsapp = () => {
|
||
Linking.openURL(
|
||
`https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(shareText)}`
|
||
);
|
||
};
|
||
|
||
return (
|
||
<Screen>
|
||
<AppHeader
|
||
title="Pharmacist code"
|
||
subtitle="Copy, share, or screenshot"
|
||
/>
|
||
<ScrollView
|
||
contentContainerStyle={{
|
||
paddingHorizontal: gutter,
|
||
paddingBottom: 48,
|
||
gap: 16,
|
||
}}
|
||
>
|
||
<Text variant="bodySm" color={colors.muted}>
|
||
Share this code with a Gishen pharmacist. They enter it on the admin page
|
||
to create your order. Pay when you collect.
|
||
</Text>
|
||
|
||
{/* Clean card — ideal for device screenshots */}
|
||
<View
|
||
style={{
|
||
borderRadius: radii.lg,
|
||
overflow: "hidden",
|
||
backgroundColor: colors.surface,
|
||
borderWidth: 1,
|
||
borderColor: colors.brandBorder,
|
||
...shadows.md,
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
backgroundColor: colors.brand,
|
||
paddingVertical: 20,
|
||
paddingHorizontal: 20,
|
||
alignItems: "center",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<Image
|
||
source={require("@/assets/logo/gishen-logo.png")}
|
||
style={{ width: 48, height: 48, borderRadius: 12 }}
|
||
resizeMode="contain"
|
||
/>
|
||
<Text variant="h3" style={{ textAlign: "center" }}>
|
||
Gishen Pharmacy
|
||
</Text>
|
||
<Text variant="caption" style={{ textAlign: "center", opacity: 0.85 }}>
|
||
Pharmacist order code
|
||
</Text>
|
||
</View>
|
||
|
||
<View style={{ padding: 22, alignItems: "center" }}>
|
||
<Text variant="caption" color={colors.subtle} style={{ letterSpacing: 1 }}>
|
||
SHOW OR SHARE THIS CODE
|
||
</Text>
|
||
<Text
|
||
variant="display"
|
||
style={{
|
||
marginTop: 10,
|
||
letterSpacing: 3,
|
||
fontFamily: fonts.bold,
|
||
fontSize: 36,
|
||
lineHeight: 42,
|
||
}}
|
||
>
|
||
{order.pickupCode}
|
||
</Text>
|
||
<Text
|
||
variant="bodySm"
|
||
color={colors.muted}
|
||
style={{ marginTop: 12, textAlign: "center" }}
|
||
>
|
||
Enter on admin → create order
|
||
</Text>
|
||
</View>
|
||
|
||
<View
|
||
style={{
|
||
borderTopWidth: 1,
|
||
borderTopColor: colors.line,
|
||
padding: 18,
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<Row label="Customer" value={order.customerName ?? "—"} />
|
||
<Row label="Phone" value={order.customerPhone ?? "—"} />
|
||
<Row label="Items" value={`${order.items.length}`} />
|
||
{order.items.slice(0, 6).map((item) => (
|
||
<Text
|
||
key={item.productId}
|
||
variant="caption"
|
||
color={colors.subtle}
|
||
style={{ paddingLeft: 2 }}
|
||
>
|
||
· {item.name} ×{item.quantity}
|
||
</Text>
|
||
))}
|
||
{order.items.length > 6 ? (
|
||
<Text variant="caption" color={colors.subtle}>
|
||
· +{order.items.length - 6} more
|
||
</Text>
|
||
) : null}
|
||
<View
|
||
style={{
|
||
marginTop: 8,
|
||
paddingTop: 12,
|
||
borderTopWidth: 1,
|
||
borderTopColor: colors.line,
|
||
flexDirection: "row",
|
||
justifyContent: "space-between",
|
||
alignItems: "baseline",
|
||
}}
|
||
>
|
||
<Text variant="label" color={colors.muted}>
|
||
Total due
|
||
</Text>
|
||
<Text variant="priceLg">{formatETB(due)}</Text>
|
||
</View>
|
||
<Text variant="caption" color={colors.subtle} style={{ marginTop: 4 }}>
|
||
Pay at branch when collecting
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 8,
|
||
paddingHorizontal: 4,
|
||
}}
|
||
>
|
||
<Ionicons name="camera-outline" size={16} color={colors.subtle} />
|
||
<Text variant="caption" color={colors.subtle} style={{ flex: 1 }}>
|
||
Tip: screenshot the card above to show in person or send as an image.
|
||
</Text>
|
||
</View>
|
||
|
||
<View style={{ gap: 10 }}>
|
||
<Button
|
||
title={copied ? "Code copied" : "Copy code"}
|
||
icon={copied ? "checkmark-circle" : "copy-outline"}
|
||
onPress={copyCode}
|
||
/>
|
||
<View style={{ flexDirection: "row", gap: 10 }}>
|
||
<View style={{ flex: 1 }}>
|
||
<Button title="Share" icon="share-outline" variant="soft" onPress={share} />
|
||
</View>
|
||
<View style={{ flex: 1 }}>
|
||
<Button
|
||
title="WhatsApp"
|
||
icon="logo-whatsapp"
|
||
variant="outline"
|
||
onPress={whatsapp}
|
||
/>
|
||
</View>
|
||
</View>
|
||
<Pressable onPress={copyFull} hitSlop={8}>
|
||
<Text
|
||
variant="label"
|
||
color={colors.brandText}
|
||
style={{ textAlign: "center", marginTop: 4 }}
|
||
>
|
||
Copy full order details
|
||
</Text>
|
||
</Pressable>
|
||
</View>
|
||
|
||
<View style={{ gap: 10, marginTop: 4 }}>
|
||
<Button
|
||
title="View order"
|
||
variant="ghost"
|
||
onPress={() => router.replace(routes.orderDetail(order.id))}
|
||
/>
|
||
<Button
|
||
title="Done — back home"
|
||
variant="dark"
|
||
onPress={() => router.replace(routes.home)}
|
||
/>
|
||
</View>
|
||
</ScrollView>
|
||
</Screen>
|
||
);
|
||
}
|
||
|
||
function Row({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<View style={{ flexDirection: "row", justifyContent: "space-between", gap: 12 }}>
|
||
<Text variant="caption" color={colors.subtle}>
|
||
{label}
|
||
</Text>
|
||
<Text variant="label" style={{ flexShrink: 1, textAlign: "right" }}>
|
||
{value}
|
||
</Text>
|
||
</View>
|
||
);
|
||
}
|