diff --git a/app/(root)/(screens)/checkout.tsx b/app/(root)/(screens)/checkout.tsx index 746ceb9..1a0905f 100644 --- a/app/(root)/(screens)/checkout.tsx +++ b/app/(root)/(screens)/checkout.tsx @@ -1,5 +1,5 @@ import { router } from "expo-router"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Alert, Linking, Pressable, ScrollView, View } from "react-native"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/Button"; @@ -10,7 +10,7 @@ import { AppHeader } from "@/components/ui/Header"; import { Screen } from "@/components/ui/Screen"; import { StickyBar } from "@/components/ui/StickyBar"; import { Text } from "@/components/ui/Text"; -import { DELIVERY_FEE, orderSubtotalTotal, voucherDiscount } from "@/lib/commerce"; +import { DELIVERY_FEE, generatePickupCode, orderSubtotalTotal, voucherDiscount } from "@/lib/commerce"; import { branches } from "@/lib/data/branches"; import { routes } from "@/lib/routes"; import { useAddressStore } from "@/lib/stores/addressStore"; @@ -18,6 +18,7 @@ import { useAuthStore } from "@/lib/stores/authStore"; import { useCartStore } from "@/lib/stores/cartStore"; import { useFamilyStore } from "@/lib/stores/familyStore"; import { useLocationStore, type PaymentMethod } from "@/lib/stores/locationStore"; +import { useNotificationStore } from "@/lib/stores/notificationStore"; import { useOrdersStore } from "@/lib/stores/ordersStore"; import { useProfileModeStore } from "@/lib/stores/profileModeStore"; import { colors, gutter } from "@/lib/theme"; @@ -38,10 +39,10 @@ export default function CheckoutScreen() { const setAreaLabel = useLocationStore((s) => s.setAreaLabel); const payment = useLocationStore((s) => s.paymentPreference); const setPayment = useLocationStore((s) => s.setPaymentPreference); - const allowed = useLocationStore((s) => s.allowedPayments()); const addresses = useAddressStore((s) => s.addresses); const defaultAddress = useAddressStore((s) => s.defaultAddress()); const addOrder = useOrdersStore((s) => s.addOrder); + const pushNotification = useNotificationStore((s) => s.push); const addPoints = useAuthStore((s) => s.addPoints); const user = useAuthStore((s) => s.user); const setActiveVoucher = useAuthStore((s) => s.setActiveVoucher); @@ -52,8 +53,9 @@ export default function CheckoutScreen() { const familyMembers = useFamilyStore((s) => s.members); const activeMemberId = useFamilyStore((s) => s.activeMemberId); const setActiveMember = useFamilyStore((s) => s.setActiveMember); + const isGuest = !!user?.isGuest; - const [name, setName] = useState(user?.name ?? ""); + const [name, setName] = useState(user?.name && !user.isGuest ? user.name : ""); const [phone, setPhone] = useState(user?.phone ?? ""); const [notes, setNotes] = useState(""); const [addressId, setAddressId] = useState(defaultAddress?.id ?? ""); @@ -61,17 +63,38 @@ export default function CheckoutScreen() { cartVoucher || user?.activeVoucher || "" ); const [forMemberId, setForMemberId] = useState(activeMemberId); + /** Guests (and optional pickup) can fulfill at any branch with a quick code */ + const [anyBranch, setAnyBranch] = useState(isGuest); + + useEffect(() => { + if (!isGuest) return; + if (fulfillment !== "pickup") setFulfillment("pickup"); + setAnyBranch(true); + if (payment !== "pay_at_branch") setPayment("pay_at_branch"); + }, [isGuest, fulfillment, payment, setFulfillment, setPayment]); const selectedAddress = useMemo( () => addresses.find((a) => a.id === addressId) ?? defaultAddress, [addresses, addressId, defaultAddress] ); - const deliveryFee = fulfillment === "delivery" && items.length ? DELIVERY_FEE : 0; + const effectiveFulfillment = isGuest ? "pickup" : fulfillment; + const useAnyBranchPickup = + effectiveFulfillment === "pickup" && (anyBranch || isGuest); + + const allowed = useMemo((): PaymentMethod[] => { + const base: PaymentMethod[] = ["chapa", "telebirr", "mpesa"]; + return effectiveFulfillment === "pickup" + ? [...base, "pay_at_branch"] + : base; + }, [effectiveFulfillment]); + + const deliveryFee = + effectiveFulfillment === "delivery" && items.length ? DELIVERY_FEE : 0; const discount = voucherDiscount(subtotal, voucher); const afterDiscount = Math.max(0, subtotal - discount); const split = - mode === "corporate" + mode === "corporate" && !isGuest ? quoteSplit(afterDiscount) : { covered: 0, selfPay: afterDiscount }; const total = orderSubtotalTotal({ @@ -86,15 +109,16 @@ export default function CheckoutScreen() { Alert.alert("Contact details", "Enter your name and a valid phone number."); return; } - if (!allowed.includes(payment)) { + if (!allowed.includes(payment) && !(useAnyBranchPickup && payment === "pay_at_branch")) { Alert.alert("Payment", "Select a valid payment method."); return; } - if (fulfillment === "pickup" && !branchId) { - Alert.alert("Pickup", "Select a branch for pickup."); + + if (effectiveFulfillment === "pickup" && !useAnyBranchPickup && !branchId) { + Alert.alert("Pickup", "Select a branch, or choose any-branch quick code."); return; } - if (fulfillment === "delivery" && !selectedAddress && !areaLabel.trim()) { + if (effectiveFulfillment === "delivery" && !selectedAddress && !areaLabel.trim()) { Alert.alert("Delivery", "Choose a saved address or enter an area."); return; } @@ -108,52 +132,91 @@ export default function CheckoutScreen() { const orderNotes = [ notes.trim(), forMember ? `For family: ${forMember.name} (${forMember.relationship})` : "", + useAnyBranchPickup ? "Quick code — redeemable at any Gishen branch" : "", ] .filter(Boolean) .join(" · "); + const pickupCode = + effectiveFulfillment === "pickup" ? generatePickupCode() : null; + const fee = effectiveFulfillment === "pickup" ? 0 : deliveryFee; + const order = addOrder({ items, subtotal, - deliveryFee, + deliveryFee: fee, discount, voucherCode: discount ? voucher.trim().toUpperCase() : null, - total: afterDiscount + deliveryFee, - fulfillment, - branchId, - areaLabel: - fulfillment === "pickup" + total: afterDiscount + fee, + fulfillment: effectiveFulfillment, + branchId: useAnyBranchPickup ? null : branchId, + areaLabel: useAnyBranchPickup + ? "Any Gishen branch" + : effectiveFulfillment === "pickup" ? branch?.area ?? areaLabel : selectedAddress?.area ?? areaLabel, - addressId: fulfillment === "delivery" ? selectedAddress?.id ?? null : null, - addressLine, + addressId: + effectiveFulfillment === "delivery" ? selectedAddress?.id ?? null : null, + addressLine: useAnyBranchPickup ? undefined : addressLine, paymentMethod: payment, corporateCovered: split.covered, - selfPay: split.selfPay + deliveryFee, + selfPay: split.selfPay + fee, customerName: name.trim(), customerPhone: phone.trim(), notes: orderNotes, riderAssigned: false, + pickupCode, + anyBranch: useAnyBranchPickup, + isGuestOrder: isGuest, + status: + useAnyBranchPickup || payment === "pay_at_branch" ? "confirmed" : undefined, }); if (forMemberId) setActiveMember(forMemberId); - if (split.covered > 0) consumeAllowance(split.covered); - addPoints(Math.max(1, Math.floor(afterDiscount / 10)), "Order reward"); + if (!isGuest && split.covered > 0) consumeAllowance(split.covered); + if (!isGuest) addPoints(Math.max(1, Math.floor(afterDiscount / 10)), "Order reward"); if (discount) { setActiveVoucher(null); setCartVoucher(null); } clear(); + pushNotification({ + kind: "order", + title: pickupCode ? `Pickup code ${pickupCode}` : "Order placed", + body: pickupCode + ? useAnyBranchPickup + ? `Show ${pickupCode} at any Gishen branch counter to collect your order.` + : `Show ${pickupCode} at ${branch?.name ?? "your branch"} to collect.` + : `Your ${order.fulfillment} order is confirmed.`, + href: routes.orderDetail(order.id), + }); + Alert.alert( - "Order placed", - payment === "pay_at_branch" - ? `Pay ${formatETB(order.selfPay ?? order.total)} at ${branch?.name ?? "the branch"}.` - : `Mock ${labelPayment(payment, t)} payment of ${formatETB(order.total)} succeeded.`, + pickupCode ? "Quick pickup code ready" : "Order placed", + pickupCode + ? `Your code is ${pickupCode}. ${ + useAnyBranchPickup + ? "Present it at any Gishen branch for fulfillment." + : `Present it at ${branch?.name ?? "the branch"}.` + }${ + payment === "pay_at_branch" + ? ` Pay ${formatETB(order.selfPay ?? order.total)} at the counter.` + : "" + }` + : payment === "pay_at_branch" + ? `Pay ${formatETB(order.selfPay ?? order.total)} at ${branch?.name ?? "the branch"}.` + : `Mock ${labelPayment(payment, t)} payment of ${formatETB(order.total)} succeeded.`, [ { text: "View order", onPress: () => router.replace(routes.orderDetail(order.id)) }, - { text: "Track", onPress: () => router.replace(routes.tracking(order.id)) }, + { + text: pickupCode ? "Done" : "Track", + onPress: () => + pickupCode + ? router.replace(routes.orderDetail(order.id)) + : router.replace(routes.tracking(order.id)), + }, ] ); }; @@ -245,16 +308,37 @@ export default function CheckoutScreen() { Fulfillment + {isGuest ? ( + + Guest quick code + + As a guest you get a short pickup code (e.g. GSH-AB12). Show it at any + Gishen branch counter — no account needed to fulfill. + + + ) : null} { + if (isGuest && v === "delivery") { + Alert.alert( + "Guest checkout", + "Guests use quick-code pickup at any branch. Sign in for delivery & loyalty." + ); + setFulfillment("pickup"); + setAnyBranch(true); + return; + } + setFulfillment(v); + if (v === "delivery") setAnyBranch(false); + }} options={[ { value: "delivery", label: t("common.delivery") }, { value: "pickup", label: t("common.pickup") }, ]} /> - {fulfillment === "delivery" ? ( + {effectiveFulfillment === "delivery" ? ( @@ -302,16 +386,42 @@ export default function CheckoutScreen() { ) : ( + { + setAnyBranch(true); + setBranchId(null); + setPayment("pay_at_branch"); + }} + style={{ + padding: 12, + borderRadius: 14, + borderWidth: 1.5, + borderColor: anyBranch ? colors.brand : colors.line, + backgroundColor: anyBranch ? colors.brandWash : colors.surface, + }} + > + Any branch · quick code + + Get a GSH-XXXX code and collect at Bole, CMC, Jemo, or any other branch + + {branches.map((b) => ( setBranchId(b.id)} + onPress={() => { + setAnyBranch(false); + setBranchId(b.id); + }} style={{ padding: 12, borderRadius: 14, borderWidth: 1.5, - borderColor: branchId === b.id ? colors.brand : colors.line, - backgroundColor: branchId === b.id ? colors.brandWash : colors.surface, + borderColor: + !anyBranch && branchId === b.id ? colors.brand : colors.line, + backgroundColor: + !anyBranch && branchId === b.id + ? colors.brandWash + : colors.surface, }} > {b.name} @@ -327,7 +437,7 @@ export default function CheckoutScreen() { {t("common.payment")} - {fulfillment === "delivery" ? ( + {effectiveFulfillment === "delivery" ? ( Pay at branch is only available for pickup @@ -394,7 +504,7 @@ export default function CheckoutScreen() { /> ) : null} diff --git a/app/(root)/(screens)/family/index.tsx b/app/(root)/(screens)/family/index.tsx index aa9ae7c..f400fac 100644 --- a/app/(root)/(screens)/family/index.tsx +++ b/app/(root)/(screens)/family/index.tsx @@ -1,94 +1,7 @@ -import { router } from "expo-router"; -import { Alert, ScrollView, View } from "react-native"; -import { useTranslation } from "react-i18next"; -import { Badge } from "@/components/ui/Badge"; -import { Button } from "@/components/ui/Button"; -import { Card } from "@/components/ui/Card"; -import { AppHeader } from "@/components/ui/Header"; -import { Screen } from "@/components/ui/Screen"; -import { Text } from "@/components/ui/Text"; -import { useFamilyStore } from "@/lib/stores/familyStore"; -import { colors, gutter } from "@/lib/theme"; +import { Redirect } from "expo-router"; import { routes } from "@/lib/routes"; -export default function FamilyScreen() { - const { t } = useTranslation(); - 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); - - return ( - - - - - - Your invite code - - - {inviteCode} - - - Share this code so family can join your care circle. - - - -