@teincfood/core 0.7.7 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/filesystem.d.ts +22 -0
- package/dist/adapters/filesystem.js +6 -0
- package/dist/adapters/print.d.ts +29 -0
- package/dist/adapters/print.js +12 -0
- package/dist/cart/hooks.d.ts +36 -0
- package/dist/cart/hooks.js +45 -0
- package/dist/cart/store.d.ts +33 -0
- package/dist/cart/store.js +122 -0
- package/dist/catalog/hooks.d.ts +25 -0
- package/dist/catalog/hooks.js +160 -0
- package/dist/hooks/orders.hooks.d.ts +16 -0
- package/dist/hooks/orders.hooks.js +93 -0
- package/dist/image-cache/service.d.ts +29 -0
- package/dist/image-cache/service.js +125 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +27 -2
- package/dist/printer/hooks.d.ts +32 -0
- package/dist/printer/hooks.js +53 -0
- package/dist/printer/receipt.d.ts +12 -0
- package/dist/printer/receipt.js +315 -0
- package/dist/printer/service.d.ts +31 -0
- package/dist/printer/service.js +119 -0
- package/dist/printer/store.d.ts +24 -0
- package/dist/printer/store.js +94 -0
- package/dist/printer/types.d.ts +12 -0
- package/dist/printer/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem adapter — host injects expo-file-system / Tauri fs
|
|
3
|
+
*/
|
|
4
|
+
export interface FileSystemAdapter {
|
|
5
|
+
getInfoAsync(path: string): Promise<{
|
|
6
|
+
exists: boolean;
|
|
7
|
+
isDirectory?: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
makeDirectoryAsync(path: string, opts?: {
|
|
10
|
+
intermediates?: boolean;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
downloadAsync(url: string, fileUri: string): Promise<{
|
|
13
|
+
uri: string;
|
|
14
|
+
}>;
|
|
15
|
+
deleteAsync(path: string, opts?: {
|
|
16
|
+
idempotent?: boolean;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
readDirectoryAsync(path: string): Promise<string[]>;
|
|
19
|
+
getDocumentDirectory(): string;
|
|
20
|
+
}
|
|
21
|
+
export declare function setFileSystemAdapter(a: FileSystemAdapter | null): void;
|
|
22
|
+
export declare function getFileSystemAdapter(): FileSystemAdapter | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print adapters — host injects platform-specific implementations.
|
|
3
|
+
*/
|
|
4
|
+
export interface TcpSocketAdapter {
|
|
5
|
+
sendToTcpPrinter(ip: string, port: number, text: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface PrintFileAdapter {
|
|
8
|
+
printToFileAsync(html: string): Promise<{
|
|
9
|
+
uri: string;
|
|
10
|
+
}>;
|
|
11
|
+
isSharingAvailable?(): Promise<boolean>;
|
|
12
|
+
shareAsync?(uri: string, options?: {
|
|
13
|
+
dialogTitle?: string;
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface MdnsAdapter {
|
|
18
|
+
discoverPrinters?(timeoutMs?: number): Promise<{
|
|
19
|
+
name: string;
|
|
20
|
+
ip: string;
|
|
21
|
+
port: number;
|
|
22
|
+
}[]>;
|
|
23
|
+
}
|
|
24
|
+
export declare function setTcpAdapter(a: TcpSocketAdapter | null): void;
|
|
25
|
+
export declare function getTcpAdapter(): TcpSocketAdapter | null;
|
|
26
|
+
export declare function setPrintFileAdapter(a: PrintFileAdapter | null): void;
|
|
27
|
+
export declare function getPrintFileAdapter(): PrintFileAdapter | null;
|
|
28
|
+
export declare function setMdnsAdapter(a: MdnsAdapter | null): void;
|
|
29
|
+
export declare function getMdnsAdapter(): MdnsAdapter | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print adapters — host injects platform-specific implementations.
|
|
3
|
+
*/
|
|
4
|
+
let tcpAdapter = null;
|
|
5
|
+
let printFileAdapter = null;
|
|
6
|
+
let mdnsAdapter = null;
|
|
7
|
+
export function setTcpAdapter(a) { tcpAdapter = a; }
|
|
8
|
+
export function getTcpAdapter() { return tcpAdapter; }
|
|
9
|
+
export function setPrintFileAdapter(a) { printFileAdapter = a; }
|
|
10
|
+
export function getPrintFileAdapter() { return printFileAdapter; }
|
|
11
|
+
export function setMdnsAdapter(a) { mdnsAdapter = a; }
|
|
12
|
+
export function getMdnsAdapter() { return mdnsAdapter; }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cart hooks — UI bindings for the device-local cart store + pricing.
|
|
3
|
+
*/
|
|
4
|
+
import { removeItem, updateQuantity, clearCart, clearAll } from "./store";
|
|
5
|
+
import type { MenuItem, MenuItemVariantResponse, MenuItemAddonResponse } from "../types/api/menu.api";
|
|
6
|
+
import type { POSCheckoutForm } from "../types/pos.types";
|
|
7
|
+
export declare function usePOSStore(): {
|
|
8
|
+
items: import("..").POSCartItem[];
|
|
9
|
+
checkoutForm: POSCheckoutForm;
|
|
10
|
+
addItem: (menuItem: MenuItem, quantity: number, variant: MenuItemVariantResponse | null, addons: MenuItemAddonResponse[]) => void;
|
|
11
|
+
removeItem: typeof removeItem;
|
|
12
|
+
updateQuantity: typeof updateQuantity;
|
|
13
|
+
updateCheckoutForm: (u: Partial<POSCheckoutForm>) => void;
|
|
14
|
+
clearCart: typeof clearCart;
|
|
15
|
+
clearAll: typeof clearAll;
|
|
16
|
+
};
|
|
17
|
+
export declare function usePOSCart(): {
|
|
18
|
+
items: import("..").POSCartItem[];
|
|
19
|
+
checkoutForm: POSCheckoutForm;
|
|
20
|
+
addItem: (menuItem: MenuItem, quantity: number, variant: MenuItemVariantResponse | null, addons: MenuItemAddonResponse[]) => void;
|
|
21
|
+
removeItem: typeof removeItem;
|
|
22
|
+
updateQuantity: typeof updateQuantity;
|
|
23
|
+
updateCheckoutForm: (u: Partial<POSCheckoutForm>) => void;
|
|
24
|
+
clearCart: typeof clearCart;
|
|
25
|
+
clearAll: typeof clearAll;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Pricing hook — returns local calculation (always available offline) and
|
|
29
|
+
* optional cloud preview; mirrors CheckoutPanel calculatePricing logic.
|
|
30
|
+
*/
|
|
31
|
+
export declare function usePOSPricing(): {
|
|
32
|
+
cart: import("./store").CartState;
|
|
33
|
+
localPricing: import("..").PricingResult | null;
|
|
34
|
+
preview: import("@tanstack/react-query").UseQueryResult<import("..").POSOrderPreviewResponse, Error>;
|
|
35
|
+
currency: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cart hooks — UI bindings for the device-local cart store + pricing.
|
|
3
|
+
*/
|
|
4
|
+
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
|
5
|
+
import { getCartSnapshot, subscribeCart, hydrateCartStore, addItem, removeItem, updateQuantity, updateCheckoutForm, clearCart, clearAll } from "./store";
|
|
6
|
+
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
7
|
+
import { usePOSOrderPreviewQuery } from "../hooks/orders.hooks";
|
|
8
|
+
import { calculatePricing } from "../pricing/engine";
|
|
9
|
+
export function usePOSStore() {
|
|
10
|
+
const state = useSyncExternalStore(subscribeCart, getCartSnapshot, getCartSnapshot);
|
|
11
|
+
useEffect(() => { void hydrateCartStore(); }, []);
|
|
12
|
+
return {
|
|
13
|
+
items: state.items,
|
|
14
|
+
checkoutForm: state.checkoutForm,
|
|
15
|
+
addItem: (menuItem, quantity, variant, addons) => addItem(menuItem, quantity, variant, addons),
|
|
16
|
+
removeItem,
|
|
17
|
+
updateQuantity,
|
|
18
|
+
updateCheckoutForm: (u) => updateCheckoutForm(u),
|
|
19
|
+
clearCart,
|
|
20
|
+
clearAll,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function usePOSCart() { return usePOSStore(); }
|
|
24
|
+
/**
|
|
25
|
+
* Pricing hook — returns local calculation (always available offline) and
|
|
26
|
+
* optional cloud preview; mirrors CheckoutPanel calculatePricing logic.
|
|
27
|
+
*/
|
|
28
|
+
export function usePOSPricing() {
|
|
29
|
+
const cart = useSyncExternalStore(subscribeCart, getCartSnapshot, getCartSnapshot);
|
|
30
|
+
const business = useSyncExternalStore(subscribeBusiness, () => getActiveBusiness(), () => null);
|
|
31
|
+
const currency = business?.currency_code ?? "GHS";
|
|
32
|
+
// Need tax rules — pull from reference_data via lazy import to avoid cycle
|
|
33
|
+
// For now, calculate without taxRules; host can pass taxRules if needed.
|
|
34
|
+
const localPricing = useMemo(() => {
|
|
35
|
+
try {
|
|
36
|
+
return calculatePricing({ items: cart.items, taxRules: [], currency_code: currency });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}, [cart.items, currency]);
|
|
42
|
+
// Cloud preview still available via dedicated hook
|
|
43
|
+
const preview = usePOSOrderPreviewQuery(cart.items, cart.checkoutForm.order_type, cart.checkoutForm.customer_name);
|
|
44
|
+
return { cart, localPricing, preview, currency };
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POS cart store — device-local, persisted via KV driver.
|
|
3
|
+
* Mirrors TeincFoodBusiness/src/stores/pos.store.ts
|
|
4
|
+
* Persist key: "Teinc Food-pos-cart" (for migration compatibility).
|
|
5
|
+
*/
|
|
6
|
+
import type { MenuItem, MenuItemVariantResponse, MenuItemAddonResponse } from "../types/api/menu.api";
|
|
7
|
+
import type { POSCartItem, POSCheckoutForm } from "../types/pos.types";
|
|
8
|
+
export interface CartState {
|
|
9
|
+
items: POSCartItem[];
|
|
10
|
+
checkoutForm: POSCheckoutForm;
|
|
11
|
+
}
|
|
12
|
+
export declare function hydrateCartStore(): Promise<void>;
|
|
13
|
+
export declare function getCartState(): CartState;
|
|
14
|
+
export declare function subscribeCart(listener: () => void): () => void;
|
|
15
|
+
export declare function getCartSnapshot(): CartState;
|
|
16
|
+
export declare function addItem(menuItem: MenuItem, quantity: number, variant: MenuItemVariantResponse | null, addons: MenuItemAddonResponse[]): void;
|
|
17
|
+
export declare function removeItem(cartItemId: string): void;
|
|
18
|
+
export declare function updateQuantity(cartItemId: string, quantity: number): void;
|
|
19
|
+
export declare function updateCheckoutForm(updates: Partial<POSCheckoutForm>): void;
|
|
20
|
+
export declare function clearCart(): void;
|
|
21
|
+
export declare function clearAll(): void;
|
|
22
|
+
export declare const cartStore: {
|
|
23
|
+
getState: typeof getCartState;
|
|
24
|
+
subscribe: typeof subscribeCart;
|
|
25
|
+
getSnapshot: typeof getCartSnapshot;
|
|
26
|
+
hydrate: typeof hydrateCartStore;
|
|
27
|
+
addItem: typeof addItem;
|
|
28
|
+
removeItem: typeof removeItem;
|
|
29
|
+
updateQuantity: typeof updateQuantity;
|
|
30
|
+
updateCheckoutForm: typeof updateCheckoutForm;
|
|
31
|
+
clearCart: typeof clearCart;
|
|
32
|
+
clearAll: typeof clearAll;
|
|
33
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POS cart store — device-local, persisted via KV driver.
|
|
3
|
+
* Mirrors TeincFoodBusiness/src/stores/pos.store.ts
|
|
4
|
+
* Persist key: "Teinc Food-pos-cart" (for migration compatibility).
|
|
5
|
+
*/
|
|
6
|
+
import { getKVDriver } from "../adapters/kv";
|
|
7
|
+
const CART_KEY = "Teinc Food-pos-cart";
|
|
8
|
+
const defaultCheckoutForm = {
|
|
9
|
+
customer_name: "",
|
|
10
|
+
customer_phone: "",
|
|
11
|
+
order_type: "takeout",
|
|
12
|
+
pos_payment_method: "cash",
|
|
13
|
+
};
|
|
14
|
+
let state = {
|
|
15
|
+
items: [],
|
|
16
|
+
checkoutForm: { ...defaultCheckoutForm },
|
|
17
|
+
};
|
|
18
|
+
let hydrated = false;
|
|
19
|
+
const listeners = new Set();
|
|
20
|
+
function notify() { listeners.forEach((l) => l()); }
|
|
21
|
+
async function persist() {
|
|
22
|
+
try {
|
|
23
|
+
const kv = getKVDriver();
|
|
24
|
+
await kv.setItem(CART_KEY, JSON.stringify({ state: { items: state.items, checkoutForm: state.checkoutForm }, version: 0 }));
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
}
|
|
28
|
+
export async function hydrateCartStore() {
|
|
29
|
+
if (hydrated)
|
|
30
|
+
return;
|
|
31
|
+
hydrated = true;
|
|
32
|
+
try {
|
|
33
|
+
const kv = getKVDriver();
|
|
34
|
+
const raw = await kv.getItem(CART_KEY);
|
|
35
|
+
if (raw) {
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
const s = parsed.state ?? parsed;
|
|
38
|
+
state = {
|
|
39
|
+
items: Array.isArray(s.items) ? s.items : [],
|
|
40
|
+
checkoutForm: s.checkoutForm ? { ...defaultCheckoutForm, ...s.checkoutForm } : { ...defaultCheckoutForm },
|
|
41
|
+
};
|
|
42
|
+
notify();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
export function getCartState() { return state; }
|
|
48
|
+
export function subscribeCart(listener) {
|
|
49
|
+
listeners.add(listener);
|
|
50
|
+
return () => listeners.delete(listener);
|
|
51
|
+
}
|
|
52
|
+
export function getCartSnapshot() { return state; }
|
|
53
|
+
function computeUnitPrice(menuItem, variant, addons) {
|
|
54
|
+
let price = menuItem?.basePrice?.amountMinor
|
|
55
|
+
?? menuItem?.base_price?.amount_minor
|
|
56
|
+
?? 0;
|
|
57
|
+
if (variant)
|
|
58
|
+
price = variant.price?.amountMinor ?? variant.price_minor ?? price;
|
|
59
|
+
// addons: amountMinor
|
|
60
|
+
for (const a of addons) {
|
|
61
|
+
const amt = a.amountMinor ?? a.amount_minor ?? 0;
|
|
62
|
+
price += amt;
|
|
63
|
+
}
|
|
64
|
+
return price;
|
|
65
|
+
}
|
|
66
|
+
function makeCartItemId(menuItemId, variantId, addonIds) {
|
|
67
|
+
const sorted = [...addonIds].sort();
|
|
68
|
+
return `${menuItemId}-${variantId || ""}-${sorted.join(",")}`;
|
|
69
|
+
}
|
|
70
|
+
export function addItem(menuItem, quantity, variant, addons) {
|
|
71
|
+
const variantId = variant?.id;
|
|
72
|
+
const addonIds = addons.map((a) => a.id);
|
|
73
|
+
const cartItemId = makeCartItemId(menuItem.id, variantId, addonIds);
|
|
74
|
+
const unitPrice = computeUnitPrice(menuItem, variant, addons);
|
|
75
|
+
const selectedAddons = addons.map((a) => ({
|
|
76
|
+
addon_id: a.id,
|
|
77
|
+
name: a.name,
|
|
78
|
+
price_minor: a.amountMinor ?? a.amount_minor ?? 0,
|
|
79
|
+
}));
|
|
80
|
+
const existing = state.items.find((i) => i.id === cartItemId);
|
|
81
|
+
if (existing) {
|
|
82
|
+
state = { ...state, items: state.items.map((i) => i.id === cartItemId ? { ...i, quantity: i.quantity + quantity } : i) };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const item = {
|
|
86
|
+
id: cartItemId,
|
|
87
|
+
menu_item: menuItem,
|
|
88
|
+
quantity,
|
|
89
|
+
selected_variant: variant,
|
|
90
|
+
selected_addons: selectedAddons,
|
|
91
|
+
notes: "",
|
|
92
|
+
unit_price_minor: unitPrice,
|
|
93
|
+
};
|
|
94
|
+
state = { ...state, items: [...state.items, item] };
|
|
95
|
+
}
|
|
96
|
+
notify();
|
|
97
|
+
void persist();
|
|
98
|
+
}
|
|
99
|
+
export function removeItem(cartItemId) {
|
|
100
|
+
state = { ...state, items: state.items.filter((i) => i.id !== cartItemId) };
|
|
101
|
+
notify();
|
|
102
|
+
void persist();
|
|
103
|
+
}
|
|
104
|
+
export function updateQuantity(cartItemId, quantity) {
|
|
105
|
+
state = { ...state, items: state.items.map((i) => i.id === cartItemId ? { ...i, quantity: Math.max(1, quantity) } : i) };
|
|
106
|
+
notify();
|
|
107
|
+
void persist();
|
|
108
|
+
}
|
|
109
|
+
export function updateCheckoutForm(updates) {
|
|
110
|
+
state = { ...state, checkoutForm: { ...state.checkoutForm, ...updates } };
|
|
111
|
+
notify();
|
|
112
|
+
void persist();
|
|
113
|
+
}
|
|
114
|
+
export function clearCart() { state = { ...state, items: [] }; notify(); void persist(); }
|
|
115
|
+
export function clearAll() { state = { items: [], checkoutForm: { ...defaultCheckoutForm } }; notify(); void persist(); }
|
|
116
|
+
export const cartStore = {
|
|
117
|
+
getState: getCartState,
|
|
118
|
+
subscribe: subscribeCart,
|
|
119
|
+
getSnapshot: getCartSnapshot,
|
|
120
|
+
hydrate: hydrateCartStore,
|
|
121
|
+
addItem, removeItem, updateQuantity, updateCheckoutForm, clearCart, clearAll,
|
|
122
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog hooks — offline-first menu browsing
|
|
3
|
+
* Port of TeincFoodBusiness/src/hooks/menu.hooks.ts
|
|
4
|
+
* Uses reference_data SQLite cache + network refresh when online.
|
|
5
|
+
*/
|
|
6
|
+
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
|
7
|
+
export declare const CATALOG_KEYS: {
|
|
8
|
+
readonly all: readonly ["menu"];
|
|
9
|
+
readonly items: (businessId?: string) => readonly ["menu", "items", string | undefined];
|
|
10
|
+
readonly categories: (businessId?: string) => readonly ["menu", "categories", string | undefined];
|
|
11
|
+
};
|
|
12
|
+
export declare function useMenuItemsQuery(filters?: {
|
|
13
|
+
search?: string;
|
|
14
|
+
category_ids?: string;
|
|
15
|
+
sort?: string;
|
|
16
|
+
}, enabled?: boolean): ReturnType<typeof useInfiniteQuery> & {
|
|
17
|
+
data: unknown[] | undefined;
|
|
18
|
+
};
|
|
19
|
+
export declare function useMenuCategoriesQuery(): ReturnType<typeof useQuery>;
|
|
20
|
+
export declare function useCreateMenuItemMutation(): import("@tanstack/react-query").UseMutationResult<unknown, Error, Record<string, unknown>, unknown>;
|
|
21
|
+
export declare function useUpdateMenuItemMutation(): import("@tanstack/react-query").UseMutationResult<unknown, Error, {
|
|
22
|
+
id: string;
|
|
23
|
+
payload: Record<string, unknown>;
|
|
24
|
+
}, unknown>;
|
|
25
|
+
export declare function useDeleteMenuItemMutation(): import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog hooks — offline-first menu browsing
|
|
3
|
+
* Port of TeincFoodBusiness/src/hooks/menu.hooks.ts
|
|
4
|
+
* Uses reference_data SQLite cache + network refresh when online.
|
|
5
|
+
*/
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { useSyncExternalStore } from "react";
|
|
9
|
+
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
10
|
+
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
11
|
+
import { menuService } from "../adapters/services";
|
|
12
|
+
import { getReferenceData } from "../reference/operations";
|
|
13
|
+
import { openSyncDatabase } from "../db/connection";
|
|
14
|
+
import { referenceSyncService } from "../reference/service";
|
|
15
|
+
import { emitReferenceChanged } from "../reference/events";
|
|
16
|
+
import { runOfflineCapableMutation } from "../sync/offline-capable";
|
|
17
|
+
export const CATALOG_KEYS = {
|
|
18
|
+
all: ["menu"],
|
|
19
|
+
items: (businessId) => [...CATALOG_KEYS.all, "items", businessId],
|
|
20
|
+
categories: (businessId) => [...CATALOG_KEYS.all, "categories", businessId],
|
|
21
|
+
};
|
|
22
|
+
function useBusinessId() {
|
|
23
|
+
const id = useSyncExternalStore(subscribeBusiness, () => getActiveBusiness()?.id ?? null, () => null);
|
|
24
|
+
return id ?? undefined;
|
|
25
|
+
}
|
|
26
|
+
function useIsOnline() {
|
|
27
|
+
const s = useSyncExternalStore(subscribeConnectivity, getConnectivityState, getConnectivityState);
|
|
28
|
+
return s.isOnline;
|
|
29
|
+
}
|
|
30
|
+
export function useMenuItemsQuery(filters, enabled = true) {
|
|
31
|
+
const businessId = useBusinessId();
|
|
32
|
+
const isOnline = useIsOnline();
|
|
33
|
+
// Cached reference query — synchronous from SQLite via query
|
|
34
|
+
const cachedQuery = useQuery({
|
|
35
|
+
queryKey: [...CATALOG_KEYS.items(businessId), "cached", filters],
|
|
36
|
+
queryFn: async () => {
|
|
37
|
+
if (!businessId)
|
|
38
|
+
return null;
|
|
39
|
+
const db = await openSyncDatabase();
|
|
40
|
+
const rec = await getReferenceData(db, businessId, "menu:items");
|
|
41
|
+
return rec?.payload ?? null;
|
|
42
|
+
},
|
|
43
|
+
enabled: enabled && !!businessId,
|
|
44
|
+
staleTime: Infinity,
|
|
45
|
+
});
|
|
46
|
+
const filtered = useMemo(() => {
|
|
47
|
+
const all = cachedQuery.data?.data ?? [];
|
|
48
|
+
if (!enabled)
|
|
49
|
+
return [];
|
|
50
|
+
let out = all;
|
|
51
|
+
if (filters?.category_ids) {
|
|
52
|
+
const ids = new Set(filters.category_ids.split(",").map((s) => s.trim()).filter(Boolean));
|
|
53
|
+
if (ids.size > 0)
|
|
54
|
+
out = out.filter((i) => ids.has((i.category_id ?? i.categoryId) ?? ""));
|
|
55
|
+
}
|
|
56
|
+
if (filters?.search) {
|
|
57
|
+
const q = filters.search.trim().toLowerCase();
|
|
58
|
+
if (q)
|
|
59
|
+
out = out.filter((i) => (i.name ?? "").toLowerCase().includes(q) || (i.description ?? "").toLowerCase().includes(q));
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}, [cachedQuery.data, filters?.category_ids, filters?.search, enabled]);
|
|
63
|
+
const networkQuery = useInfiniteQuery({
|
|
64
|
+
queryKey: [...CATALOG_KEYS.items(businessId), filters, "network"],
|
|
65
|
+
queryFn: ({ pageParam = 1 }) => menuService.fetchBusinessMenuItems(businessId, pageParam, filters),
|
|
66
|
+
initialPageParam: 1,
|
|
67
|
+
getNextPageParam: (lastPage) => {
|
|
68
|
+
const p = lastPage?.pagination;
|
|
69
|
+
return p && p.page < p.total_pages ? p.page + 1 : undefined;
|
|
70
|
+
},
|
|
71
|
+
enabled: enabled && !!businessId && isOnline,
|
|
72
|
+
staleTime: 30 * 1000,
|
|
73
|
+
retry: false,
|
|
74
|
+
networkMode: "offlineFirst",
|
|
75
|
+
});
|
|
76
|
+
const networkData = networkQuery.data?.pages
|
|
77
|
+
? networkQuery.data.pages.flatMap((p) => p.data)
|
|
78
|
+
: undefined;
|
|
79
|
+
const data = isOnline && networkData !== undefined ? networkData : enabled ? filtered : undefined;
|
|
80
|
+
return {
|
|
81
|
+
data,
|
|
82
|
+
isPending: cachedQuery.isPending && !networkData,
|
|
83
|
+
isFetching: networkQuery.isFetching,
|
|
84
|
+
isError: cachedQuery.isError || networkQuery.isError,
|
|
85
|
+
error: (cachedQuery.error ?? networkQuery.error),
|
|
86
|
+
refetch: async () => {
|
|
87
|
+
if (!businessId)
|
|
88
|
+
return;
|
|
89
|
+
if (isOnline)
|
|
90
|
+
await referenceSyncService.sync({ businessId, keys: ["menu:items", "menu:categories"] });
|
|
91
|
+
return networkQuery.refetch();
|
|
92
|
+
},
|
|
93
|
+
fetchNextPage: networkQuery.fetchNextPage,
|
|
94
|
+
hasNextPage: networkQuery.hasNextPage,
|
|
95
|
+
isFetchingNextPage: networkQuery.isFetchingNextPage,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function useMenuCategoriesQuery() {
|
|
99
|
+
const businessId = useBusinessId();
|
|
100
|
+
const isOnline = useIsOnline();
|
|
101
|
+
const cachedQuery = useQuery({
|
|
102
|
+
queryKey: [...CATALOG_KEYS.categories(businessId), "cached"],
|
|
103
|
+
queryFn: async () => {
|
|
104
|
+
if (!businessId)
|
|
105
|
+
return null;
|
|
106
|
+
const db = await openSyncDatabase();
|
|
107
|
+
const rec = await getReferenceData(db, businessId, "menu:categories");
|
|
108
|
+
return rec?.payload ?? null;
|
|
109
|
+
},
|
|
110
|
+
enabled: !!businessId,
|
|
111
|
+
staleTime: Infinity,
|
|
112
|
+
});
|
|
113
|
+
const networkQuery = useQuery({
|
|
114
|
+
queryKey: CATALOG_KEYS.categories(businessId),
|
|
115
|
+
queryFn: () => menuService.fetchBusinessCategories(businessId),
|
|
116
|
+
enabled: !!businessId && isOnline,
|
|
117
|
+
staleTime: 30 * 1000,
|
|
118
|
+
retry: false,
|
|
119
|
+
networkMode: "offlineFirst",
|
|
120
|
+
});
|
|
121
|
+
const data = (isOnline && networkQuery.data ? networkQuery.data : cachedQuery.data);
|
|
122
|
+
return { ...networkQuery, data, isPending: cachedQuery.isPending && !networkQuery.data };
|
|
123
|
+
}
|
|
124
|
+
// Mutations — offline-capable
|
|
125
|
+
export function useCreateMenuItemMutation() {
|
|
126
|
+
const businessId = useBusinessId();
|
|
127
|
+
const qc = useQueryClient();
|
|
128
|
+
return useMutation({
|
|
129
|
+
mutationFn: (payload) => runOfflineCapableMutation({ run: () => menuService.createMenuItem(payload), commandType: "menu:create_item", businessId: businessId, payload }),
|
|
130
|
+
onSuccess: () => {
|
|
131
|
+
qc.invalidateQueries({ queryKey: CATALOG_KEYS.items(businessId) });
|
|
132
|
+
if (businessId)
|
|
133
|
+
emitReferenceChanged(businessId, ["menu:items"]);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export function useUpdateMenuItemMutation() {
|
|
138
|
+
const businessId = useBusinessId();
|
|
139
|
+
const qc = useQueryClient();
|
|
140
|
+
return useMutation({
|
|
141
|
+
mutationFn: ({ id, payload }) => runOfflineCapableMutation({ run: () => menuService.updateMenuItem(id, payload), commandType: "menu:update_item", businessId: businessId, entityId: id, payload }),
|
|
142
|
+
onSuccess: () => {
|
|
143
|
+
qc.invalidateQueries({ queryKey: CATALOG_KEYS.items(businessId) });
|
|
144
|
+
if (businessId)
|
|
145
|
+
emitReferenceChanged(businessId, ["menu:items"]);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export function useDeleteMenuItemMutation() {
|
|
150
|
+
const businessId = useBusinessId();
|
|
151
|
+
const qc = useQueryClient();
|
|
152
|
+
return useMutation({
|
|
153
|
+
mutationFn: (id) => runOfflineCapableMutation({ run: () => menuService.deleteMenuItem(id), commandType: "menu:delete_item", businessId: businessId, entityId: id, payload: { id } }),
|
|
154
|
+
onSuccess: () => {
|
|
155
|
+
qc.invalidateQueries({ queryKey: CATALOG_KEYS.items(businessId) });
|
|
156
|
+
if (businessId)
|
|
157
|
+
emitReferenceChanged(businessId, ["menu:items"]);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -45,9 +45,25 @@ export declare function useSellerOrdersQuery(enabled?: boolean, filters?: {
|
|
|
45
45
|
export declare function usePendingOrders(businessId?: string): SellerOrder[];
|
|
46
46
|
export declare function useCreatePOSOrderMutation(): import("@tanstack/react-query").UseMutationResult<unknown, Error, POSOrderCreateRequest, unknown>;
|
|
47
47
|
export declare function usePOSOrderPreviewQuery(items: POSCartItem[], orderType: POSOrderType, customerName: string): import("@tanstack/react-query").UseQueryResult<POSOrderPreviewResponse, Error>;
|
|
48
|
+
export declare function useSellerOrderQuery(orderId: string, enabled?: boolean): import("@tanstack/react-query").UseQueryResult<{
|
|
49
|
+
data: SellerOrder;
|
|
50
|
+
}, Error>;
|
|
51
|
+
export declare function isQueuedTransition(result: unknown): boolean;
|
|
48
52
|
export declare const useAcceptOrderMutation: () => import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
|
|
49
53
|
export declare const useRejectOrderMutation: () => import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
|
|
50
54
|
export declare const useStartPreparingOrderMutation: () => import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
|
|
51
55
|
export declare const useMarkOrderReadyMutation: () => import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
|
|
52
56
|
export declare const useCompletePickupMutation: () => import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
|
|
53
57
|
export declare const useCancelOrderMutation: () => import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
|
|
58
|
+
export declare function useConfirmHandoverMutation(): import("@tanstack/react-query").UseMutationResult<{
|
|
59
|
+
id: string;
|
|
60
|
+
status: string;
|
|
61
|
+
}, Error, string, unknown>;
|
|
62
|
+
export declare function useAssignRiderMutation(): import("@tanstack/react-query").UseMutationResult<unknown, Error, {
|
|
63
|
+
orderId: string;
|
|
64
|
+
riderId: string;
|
|
65
|
+
}, unknown>;
|
|
66
|
+
export declare function useUpdateOrderStatusMutation(): {
|
|
67
|
+
mutate: (orderId: string, status: string) => Promise<unknown>;
|
|
68
|
+
isPending: boolean;
|
|
69
|
+
};
|
|
@@ -272,6 +272,45 @@ export function usePOSOrderPreviewQuery(items, orderType, customerName) {
|
|
|
272
272
|
staleTime: 30 * 1000,
|
|
273
273
|
});
|
|
274
274
|
}
|
|
275
|
+
// ─── Seller order detail (offline via snapshot fallback) ───────────────────
|
|
276
|
+
export function useSellerOrderQuery(orderId, enabled = true) {
|
|
277
|
+
const businessId = useBusinessId();
|
|
278
|
+
const connectivity = useConnectivity();
|
|
279
|
+
const offline = isOfflineState(connectivity);
|
|
280
|
+
return useQuery({
|
|
281
|
+
queryKey: SELLER_KEYS.order(orderId, businessId),
|
|
282
|
+
queryFn: async () => {
|
|
283
|
+
if (offline) {
|
|
284
|
+
const list = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
|
|
285
|
+
const found = list?.find((o) => o.id === orderId || o.order_number === orderId);
|
|
286
|
+
if (found)
|
|
287
|
+
return { data: found };
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const res = await sellerService.fetchSellerOrder(orderId);
|
|
291
|
+
return res;
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
const list = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
|
|
295
|
+
const found = list?.find((o) => o.id === orderId || o.order_number === orderId);
|
|
296
|
+
if (found)
|
|
297
|
+
return { data: found };
|
|
298
|
+
throw e;
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
enabled: enabled && !!orderId && !!businessId,
|
|
302
|
+
staleTime: 30 * 1000,
|
|
303
|
+
retry: false,
|
|
304
|
+
networkMode: "offlineFirst",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// ─── Helpers for queued UI ───────────────────────────────────────────────
|
|
308
|
+
export function isQueuedTransition(result) {
|
|
309
|
+
const r = result;
|
|
310
|
+
if (!r)
|
|
311
|
+
return false;
|
|
312
|
+
return r.status === "pending" && !!r.command_id && !r.id;
|
|
313
|
+
}
|
|
275
314
|
// ─── Order status mutations via SyncEngine (all go through outbox) ────────
|
|
276
315
|
function makeOrderActionMutation(build) {
|
|
277
316
|
return function useOrderActionMutation() {
|
|
@@ -298,3 +337,57 @@ export const useStartPreparingOrderMutation = makeOrderActionMutation(({ orderId
|
|
|
298
337
|
export const useMarkOrderReadyMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.markOrderReady({ orderId, businessId }));
|
|
299
338
|
export const useCompletePickupMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.completePickup({ orderId, businessId }));
|
|
300
339
|
export const useCancelOrderMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.cancelOrder({ orderId, businessId }));
|
|
340
|
+
export function useConfirmHandoverMutation() {
|
|
341
|
+
const businessId = useBusinessId();
|
|
342
|
+
const queryClient = useQueryClient();
|
|
343
|
+
return useMutation({
|
|
344
|
+
mutationFn: (deliveryId) => {
|
|
345
|
+
if (!businessId)
|
|
346
|
+
throw new Error("No active business");
|
|
347
|
+
return orderRepository.confirmHandover({ deliveryId, businessId });
|
|
348
|
+
},
|
|
349
|
+
onSettled: (_d, _e, deliveryId) => {
|
|
350
|
+
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
351
|
+
queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
|
|
352
|
+
if (deliveryId)
|
|
353
|
+
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.order(deliveryId) });
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
export function useAssignRiderMutation() {
|
|
358
|
+
const businessId = useBusinessId();
|
|
359
|
+
const queryClient = useQueryClient();
|
|
360
|
+
return useMutation({
|
|
361
|
+
mutationFn: ({ orderId, riderId }) => {
|
|
362
|
+
if (!businessId)
|
|
363
|
+
throw new Error("No active business");
|
|
364
|
+
return orderRepository.assignRider({ orderId, riderId, businessId });
|
|
365
|
+
},
|
|
366
|
+
onSettled: () => {
|
|
367
|
+
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
368
|
+
queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
export function useUpdateOrderStatusMutation() {
|
|
373
|
+
const accept = useAcceptOrderMutation();
|
|
374
|
+
const reject = useRejectOrderMutation();
|
|
375
|
+
const preparing = useStartPreparingOrderMutation();
|
|
376
|
+
const ready = useMarkOrderReadyMutation();
|
|
377
|
+
const complete = useCompletePickupMutation();
|
|
378
|
+
const cancel = useCancelOrderMutation();
|
|
379
|
+
return {
|
|
380
|
+
mutate: (orderId, status) => {
|
|
381
|
+
switch (status) {
|
|
382
|
+
case "accepted": return accept.mutateAsync(orderId);
|
|
383
|
+
case "preparing": return preparing.mutateAsync(orderId);
|
|
384
|
+
case "ready": return ready.mutateAsync(orderId);
|
|
385
|
+
case "delivered": return complete.mutateAsync(orderId);
|
|
386
|
+
case "cancelled": return cancel.mutateAsync(orderId);
|
|
387
|
+
case "rejected": return reject.mutateAsync(orderId);
|
|
388
|
+
default: throw new Error(`Unknown status ${status}`);
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
isPending: accept.isPending || reject.isPending || preparing.isPending || ready.isPending || complete.isPending || cancel.isPending,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image cache for offline reference — port of Business src/reference/image-cache.ts
|
|
3
|
+
* Durable documentDirectory cache (not cacheDirectory) with stable presigned URL keying.
|
|
4
|
+
* Host injects FileSystemAdapter; falls back to no-op on web.
|
|
5
|
+
*/
|
|
6
|
+
export interface ImageCacheEntry {
|
|
7
|
+
remoteUrl: string;
|
|
8
|
+
localUri: string | null;
|
|
9
|
+
etag: string | null;
|
|
10
|
+
lastModified: string | null;
|
|
11
|
+
cachedAt: string | null;
|
|
12
|
+
}
|
|
13
|
+
export declare function stableCacheUrl(url: string): string;
|
|
14
|
+
export declare function getCachedImagePath(url: string): string;
|
|
15
|
+
export declare function ensureImageCacheDir(): Promise<string>;
|
|
16
|
+
export declare function getCachedImageInfo(url: string): Promise<ImageCacheEntry>;
|
|
17
|
+
export declare function cacheRemoteImage(url: string | null | undefined, opts?: {
|
|
18
|
+
skipIfCached?: boolean;
|
|
19
|
+
removeOnFailure?: boolean;
|
|
20
|
+
}): Promise<ImageCacheEntry | null>;
|
|
21
|
+
export declare function cacheImageBatch(urls: (string | null | undefined)[], opts?: {
|
|
22
|
+
skipIfCached?: boolean;
|
|
23
|
+
removeOnFailure?: boolean;
|
|
24
|
+
}): Promise<ImageCacheEntry[]>;
|
|
25
|
+
export declare function clearImageCache(): Promise<void>;
|
|
26
|
+
export declare function pruneImageCache(knownUrls: Iterable<string | null | undefined>, maxAgeDays?: number): Promise<number>;
|
|
27
|
+
export declare function resolveImageSource(url: string | null | undefined): Promise<{
|
|
28
|
+
uri: string;
|
|
29
|
+
} | null>;
|