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>
188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
import { create } from "zustand";
|
|
import { createJSONStorage, persist } from "zustand/middleware";
|
|
import { seedOrders } from "@/lib/data/seed";
|
|
import type { CartItem } from "@/lib/stores/cartStore";
|
|
import type { Fulfillment, PaymentMethod } from "@/lib/stores/locationStore";
|
|
|
|
export type OrderStatus =
|
|
| "requested"
|
|
| "confirmed"
|
|
| "paid"
|
|
| "dispatched"
|
|
| "delivered"
|
|
| "completed"
|
|
| "cancelled";
|
|
|
|
export type Order = {
|
|
id: string;
|
|
createdAt: string;
|
|
items: CartItem[];
|
|
subtotal: number;
|
|
deliveryFee: number;
|
|
discount: number;
|
|
voucherCode?: string | null;
|
|
total: number;
|
|
fulfillment: Fulfillment;
|
|
branchId?: string | null;
|
|
areaLabel?: string;
|
|
addressId?: string | null;
|
|
addressLine?: string;
|
|
paymentMethod: PaymentMethod;
|
|
status: OrderStatus;
|
|
riderAssigned?: boolean;
|
|
corporateCovered?: number;
|
|
selfPay?: number;
|
|
customerName?: string;
|
|
customerPhone?: string;
|
|
notes?: string;
|
|
/** Guest / pickup QR-style code shown at any branch counter */
|
|
pickupCode?: string | null;
|
|
/** When true, order can be fulfilled at any Gishen branch */
|
|
anyBranch?: boolean;
|
|
isGuestOrder?: boolean;
|
|
timeline?: { at: string; label: string; status: OrderStatus | "created" }[];
|
|
};
|
|
|
|
type OrdersState = {
|
|
orders: Order[];
|
|
addOrder: (
|
|
order: Omit<Order, "id" | "createdAt" | "status" | "timeline" | "discount"> & {
|
|
discount?: number;
|
|
status?: OrderStatus;
|
|
}
|
|
) => Order;
|
|
updateStatus: (id: string, status: OrderStatus) => void;
|
|
cancelOrder: (id: string) => void;
|
|
getById: (id: string) => Order | undefined;
|
|
getByPickupCode: (code: string) => Order | undefined;
|
|
};
|
|
|
|
function timelineFor(status: OrderStatus, createdAt: string) {
|
|
const steps: Order["timeline"] = [
|
|
{ at: createdAt, label: "Order placed", status: "created" },
|
|
];
|
|
const add = (label: string, s: OrderStatus, hours: number) => {
|
|
const d = new Date(createdAt);
|
|
d.setHours(d.getHours() + hours);
|
|
steps!.push({ at: d.toISOString(), label, status: s });
|
|
};
|
|
if (["confirmed", "paid", "dispatched", "delivered", "completed"].includes(status)) {
|
|
add("Confirmed by pharmacy", "confirmed", 0.2);
|
|
}
|
|
if (["paid", "dispatched", "delivered", "completed"].includes(status) && status !== "confirmed") {
|
|
add("Payment received", "paid", 0.4);
|
|
}
|
|
if (["dispatched", "delivered", "completed"].includes(status)) {
|
|
add("Out for delivery / ready for pickup", "dispatched", 1);
|
|
}
|
|
if (["delivered", "completed"].includes(status)) {
|
|
add("Delivered / collected", "delivered", 3);
|
|
}
|
|
if (status === "cancelled") {
|
|
steps.push({
|
|
at: new Date().toISOString(),
|
|
label: "Order cancelled",
|
|
status: "cancelled",
|
|
});
|
|
}
|
|
return steps;
|
|
}
|
|
|
|
export const useOrdersStore = create<OrdersState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
orders: seedOrders.map((o) => ({
|
|
...o,
|
|
discount: o.discount ?? 0,
|
|
customerName: o.customerName ?? "Kirukib Abebe",
|
|
customerPhone: o.customerPhone ?? "+251911000000",
|
|
notes: o.notes ?? "",
|
|
timeline: o.timeline ?? timelineFor(o.status, o.createdAt),
|
|
})),
|
|
addOrder: (payload) => {
|
|
const createdAt = new Date().toISOString();
|
|
const discount = payload.discount ?? 0;
|
|
const status =
|
|
payload.status ??
|
|
(payload.paymentMethod === "pay_at_branch" ? "confirmed" : "paid");
|
|
const order: Order = {
|
|
...payload,
|
|
discount,
|
|
id: `ord_${Date.now()}`,
|
|
createdAt,
|
|
status,
|
|
riderAssigned: false,
|
|
timeline: timelineFor(status, createdAt),
|
|
};
|
|
set({ orders: [order, ...get().orders] });
|
|
return order;
|
|
},
|
|
updateStatus: (id, status) =>
|
|
set({
|
|
orders: get().orders.map((o) => {
|
|
if (o.id !== id) return o;
|
|
const next = {
|
|
...o,
|
|
status,
|
|
riderAssigned:
|
|
status === "dispatched" || status === "delivered"
|
|
? true
|
|
: o.riderAssigned,
|
|
};
|
|
const stamp = new Date().toISOString();
|
|
const label =
|
|
status === "dispatched"
|
|
? "Rider assigned / ready"
|
|
: status === "delivered"
|
|
? "Delivered / collected"
|
|
: status === "cancelled"
|
|
? "Order cancelled"
|
|
: `Status: ${status}`;
|
|
return {
|
|
...next,
|
|
timeline: [...(o.timeline ?? []), { at: stamp, label, status }],
|
|
};
|
|
}),
|
|
}),
|
|
cancelOrder: (id) => get().updateStatus(id, "cancelled"),
|
|
getById: (id) => get().orders.find((o) => o.id === id),
|
|
getByPickupCode: (code) => {
|
|
const normalized = code.trim().toUpperCase();
|
|
if (!normalized) return undefined;
|
|
return get().orders.find(
|
|
(o) => (o.pickupCode ?? "").toUpperCase() === normalized
|
|
);
|
|
},
|
|
}),
|
|
{
|
|
name: "gishen-orders",
|
|
storage: createJSONStorage(() => AsyncStorage),
|
|
version: 4,
|
|
migrate: (persisted: unknown) => {
|
|
const state = (persisted ?? {}) as Partial<OrdersState>;
|
|
if (!state.orders?.length) {
|
|
return {
|
|
...state,
|
|
orders: seedOrders.map((o) => ({
|
|
...o,
|
|
discount: o.discount ?? 0,
|
|
customerName: o.customerName ?? "Kirukib Abebe",
|
|
customerPhone: o.customerPhone ?? "+251911000000",
|
|
timeline: timelineFor(o.status, o.createdAt),
|
|
})),
|
|
};
|
|
}
|
|
return {
|
|
...state,
|
|
orders: state.orders.map((o) => ({
|
|
...o,
|
|
discount: o.discount ?? 0,
|
|
timeline: o.timeline ?? timelineFor(o.status, o.createdAt),
|
|
})),
|
|
};
|
|
},
|
|
}
|
|
)
|
|
);
|