@teincfood/core 0.1.4 → 0.1.6

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.
@@ -46,6 +46,7 @@ export declare const businessService: {
46
46
  fetchBusinessTaxRules: (_businessId: string) => Promise<unknown>;
47
47
  fetchBusinessById: (_businessId: string) => Promise<unknown>;
48
48
  fetchOperatingHours: (_businessId: string) => Promise<unknown>;
49
+ fetchMyBusinessMembership: (_businessId: string) => Promise<unknown>;
49
50
  };
50
51
  export declare const rolesService: {
51
52
  fetchBusinessRoles: () => Promise<unknown>;
@@ -81,6 +81,9 @@ export const businessService = {
81
81
  fetchOperatingHours: async (_businessId) => {
82
82
  notImpl("businessService.fetchOperatingHours");
83
83
  },
84
+ fetchMyBusinessMembership: async (_businessId) => {
85
+ notImpl("businessService.fetchMyBusinessMembership");
86
+ },
84
87
  };
85
88
  export const rolesService = {
86
89
  fetchBusinessRoles: async () => {
package/dist/index.d.ts CHANGED
@@ -20,6 +20,8 @@ export { formatDate, formatRelativeTime } from "./utils/formatDate";
20
20
  export * from "./utils/validation";
21
21
  export * from "./utils/responsive";
22
22
  export * from "./utils/copyToClipBoard";
23
+ export * from "./utils/customer-display";
24
+ export * from "./utils/order-filter";
23
25
  export * from "./db/migrations";
24
26
  export * from "./db/operations";
25
27
  export * as dbOps from "./db/operations";
@@ -54,6 +56,7 @@ export { emitReferenceChanged, applyReferenceChangeEvent, syncReferenceDataOnRec
54
56
  export * from "./reference/events";
55
57
  export * from "./reference/service";
56
58
  export { referenceSyncService } from "./reference/service";
59
+ export * from "./reference/capabilities";
57
60
  export * from "./adapters/sqlite";
58
61
  export * from "./adapters/kv";
59
62
  export * from "./adapters/connectivity";
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ export { formatDate, formatRelativeTime } from "./utils/formatDate";
21
21
  export * from "./utils/validation";
22
22
  export * from "./utils/responsive";
23
23
  export * from "./utils/copyToClipBoard";
24
+ export * from "./utils/customer-display";
25
+ export * from "./utils/order-filter";
24
26
  // ── DB ──
25
27
  export * from "./db/migrations";
26
28
  export * from "./db/operations";
@@ -55,6 +57,7 @@ export { emitReferenceChanged, applyReferenceChangeEvent, syncReferenceDataOnRec
55
57
  export * from "./reference/events";
56
58
  export * from "./reference/service";
57
59
  export { referenceSyncService } from "./reference/service";
60
+ export * from "./reference/capabilities";
58
61
  // ── Adapters (for host apps to supply) ──
59
62
  export * from "./adapters/sqlite";
60
63
  export * from "./adapters/kv";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Capabilities reference — offline-first.
3
+ *
4
+ * Standardises business-membership capabilities for both mobile and desktop.
5
+ * The backend `GET /businesses/:id/my-membership` is the source of truth;
6
+ * the result is mirrored to `reference_data` key `capabilities` so drawer/nav
7
+ * filtering works offline and survives restarts.
8
+ *
9
+ * Host apps wire `businessService.fetchMyBusinessMembership` via adapters/services.
10
+ */
11
+ /**
12
+ * Standard hook for both apps — businessId comes from the host's active business store.
13
+ * Returns the same shape as the previous app-level useMyBusinessCapabilities.
14
+ */
15
+ export declare function useBusinessCapabilities(businessId?: string | null): import("@tanstack/react-query").UseQueryResult<{
16
+ role: string;
17
+ source: string;
18
+ capabilities: string[];
19
+ status: string;
20
+ }, Error>;
21
+ export declare function useHasCapabilityFor(businessId: string | undefined, capability: string): boolean;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Capabilities reference — offline-first.
3
+ *
4
+ * Standardises business-membership capabilities for both mobile and desktop.
5
+ * The backend `GET /businesses/:id/my-membership` is the source of truth;
6
+ * the result is mirrored to `reference_data` key `capabilities` so drawer/nav
7
+ * filtering works offline and survives restarts.
8
+ *
9
+ * Host apps wire `businessService.fetchMyBusinessMembership` via adapters/services.
10
+ */
11
+ import { useEffect, useState } from "react";
12
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
13
+ import { openSyncDatabase } from "../db/connection";
14
+ import { getReferenceData, setReferenceData } from "./operations";
15
+ import { businessService } from "../adapters/services";
16
+ const MEMBERSHIP_KEYS = {
17
+ all: ["my-membership"],
18
+ byBusiness: (id) => [...MEMBERSHIP_KEYS.all, id],
19
+ };
20
+ function useCapabilitiesPlaceholder(businessId) {
21
+ const [cached, setCached] = useState(null);
22
+ useEffect(() => {
23
+ if (!businessId) {
24
+ setCached(null);
25
+ return;
26
+ }
27
+ let cancelled = false;
28
+ (async () => {
29
+ try {
30
+ const db = await openSyncDatabase();
31
+ const rec = await getReferenceData(db, businessId, "capabilities");
32
+ if (!cancelled && rec?.payload)
33
+ setCached(rec.payload);
34
+ }
35
+ catch { }
36
+ })();
37
+ return () => {
38
+ cancelled = true;
39
+ };
40
+ }, [businessId]);
41
+ return cached;
42
+ }
43
+ /**
44
+ * Standard hook for both apps — businessId comes from the host's active business store.
45
+ * Returns the same shape as the previous app-level useMyBusinessCapabilities.
46
+ */
47
+ export function useBusinessCapabilities(businessId) {
48
+ const queryClient = useQueryClient();
49
+ void queryClient;
50
+ const cached = useCapabilitiesPlaceholder(businessId ?? null);
51
+ return useQuery({
52
+ queryKey: MEMBERSHIP_KEYS.byBusiness(businessId ?? ""),
53
+ queryFn: async () => {
54
+ const res = (await businessService.fetchMyBusinessMembership(businessId));
55
+ try {
56
+ const db = await openSyncDatabase();
57
+ await setReferenceData(db, businessId, {
58
+ key: "capabilities",
59
+ version: `membership-${Date.now()}`,
60
+ payload: res,
61
+ downloadedAt: new Date().toISOString(),
62
+ });
63
+ }
64
+ catch { }
65
+ return res;
66
+ },
67
+ enabled: !!businessId,
68
+ staleTime: Infinity,
69
+ placeholderData: cached ?? undefined,
70
+ select: (data) => ({
71
+ role: data.data.role,
72
+ source: data.data.source,
73
+ capabilities: data.data.capabilities,
74
+ status: data.data.status,
75
+ }),
76
+ });
77
+ }
78
+ export function useHasCapabilityFor(businessId, capability) {
79
+ const { data } = useBusinessCapabilities(businessId);
80
+ return data?.capabilities.includes(capability) ?? false;
81
+ }
@@ -23,6 +23,7 @@ const DEFAULT_SYNC_KEYS = [
23
23
  "business:profile",
24
24
  "operating-hours",
25
25
  "subscription:entitlements",
26
+ "capabilities",
26
27
  ];
27
28
  class ReferenceSyncService {
28
29
  constructor() {
@@ -204,6 +205,13 @@ class ReferenceSyncService {
204
205
  version: this.hashPayload(response),
205
206
  };
206
207
  }
208
+ case "capabilities": {
209
+ const response = await businessService.fetchMyBusinessMembership(businessId);
210
+ return {
211
+ payload: response,
212
+ version: this.hashPayload(response),
213
+ };
214
+ }
207
215
  default:
208
216
  throw new Error(`No fallback fetch available for ${key}`);
209
217
  }
@@ -8,7 +8,7 @@
8
8
  * It is synchronized separately from transactional data through the
9
9
  * ReferenceSyncService.
10
10
  */
11
- export type ReferenceDataKey = "menu:items" | "menu:categories" | "roles" | "members" | "tax-rules" | "business:profile" | "operating-hours" | "subscription:entitlements";
11
+ export type ReferenceDataKey = "menu:items" | "menu:categories" | "roles" | "members" | "tax-rules" | "business:profile" | "operating-hours" | "subscription:entitlements" | "capabilities";
12
12
  export interface ReferenceDataRecord<T = unknown> {
13
13
  key: ReferenceDataKey;
14
14
  version: string;
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { Command, DomainEvent, PersistedCommand } from "./types";
12
12
  import type { SyncEngine } from "./engine";
13
+ export declare function getQueuedStatus(commandType: string): string | null;
13
14
  declare class CommandQueueService {
14
15
  private replaying;
15
16
  private syncEngine;
@@ -14,8 +14,34 @@ import { syncLogger } from "../utils/sync-logger";
14
14
  import { generateEventId } from "../utils/uuid";
15
15
  import { generateLocalOrderNumber, generateLocalCalloutNumber, } from "./order-number";
16
16
  const MAX_RETRIES = 5;
17
+ const QUEUED_STATUS_BY_COMMAND = {
18
+ "order:accept": "accepted",
19
+ "order:reject": "cancelled",
20
+ "order:start_preparing": "preparing",
21
+ "order:mark_ready": "ready",
22
+ "order:complete_pickup": "delivered",
23
+ "order:cancel": "cancelled",
24
+ "delivery:assign": "assigned",
25
+ "delivery:confirm_handover": "delivered",
26
+ };
27
+ export function getQueuedStatus(commandType) {
28
+ return QUEUED_STATUS_BY_COMMAND[commandType] ?? null;
29
+ }
17
30
  function buildOptimisticEvent(command) {
18
31
  const payload = command.payload;
32
+ if (command.command_type === "order:create") {
33
+ return {
34
+ event_id: generateEventId(),
35
+ event_type: `${command.command_type.replace(":", ".")}_queued`,
36
+ event_version: "1.0",
37
+ entity_id: command.entity_id ?? command.command_id,
38
+ user_id: command.user_id,
39
+ device_id: command.device_id,
40
+ payload: buildOptimisticOrder(command, payload),
41
+ timestamp: new Date().toISOString(),
42
+ };
43
+ }
44
+ const queuedStatus = getQueuedStatus(command.command_type);
19
45
  return {
20
46
  event_id: generateEventId(),
21
47
  event_type: `${command.command_type.replace(":", ".")}_queued`,
@@ -23,13 +49,15 @@ function buildOptimisticEvent(command) {
23
49
  entity_id: command.entity_id ?? command.command_id,
24
50
  user_id: command.user_id,
25
51
  device_id: command.device_id,
26
- payload: command.command_type === "order:create"
27
- ? buildOptimisticOrder(command, payload)
28
- : {
29
- ...payload,
30
- command_id: command.command_id,
31
- status: "pending",
32
- },
52
+ payload: {
53
+ ...payload,
54
+ command_id: command.command_id,
55
+ status: "pending",
56
+ // Used by offline read-model patching so the list reflects the queued transition.
57
+ to_status: queuedStatus,
58
+ order_id: command.entity_id,
59
+ delivery_id: command.entity_id,
60
+ },
33
61
  timestamp: new Date().toISOString(),
34
62
  };
35
63
  }
@@ -136,17 +164,17 @@ class CommandQueueService {
136
164
  order_number: event.payload.order_number ?? null,
137
165
  status: event.payload.status,
138
166
  });
139
- // Persist the optimistic order so it survives app restarts and can be
140
- // rehydrated (and shown) while still offline.
141
- if (preparedCommand.command_type === "order:create") {
142
- try {
143
- await insertEvent(db, { event, origin: "local" });
144
- }
145
- catch (error) {
146
- syncLogger.error("CommandQueueService", "Failed to persist optimistic event", {
147
- error: String(error),
148
- });
149
- }
167
+ // Persist optimistic events so they survive restarts and can be
168
+ // rehydrated offline. For order:create this is the full order; for
169
+ // transitions it carries to_status so the read model can reflect the
170
+ // queued status immediately.
171
+ try {
172
+ await insertEvent(db, { event, origin: "local" });
173
+ }
174
+ catch (error) {
175
+ syncLogger.error("CommandQueueService", "Failed to persist optimistic event", {
176
+ error: String(error),
177
+ });
150
178
  }
151
179
  return event;
152
180
  }
@@ -188,6 +216,24 @@ class CommandQueueService {
188
216
  }
189
217
  catch (error) {
190
218
  const message = error instanceof Error ? error.message : String(error);
219
+ // Distinguish retryable (network/5xx/timeout) vs non-retryable (4xx validation)
220
+ // so a rejected order does not block the entire outbox.
221
+ const apiErr = error;
222
+ const isNonRetryable = apiErr?.name === "ApiError" &&
223
+ typeof apiErr.status === "number" &&
224
+ apiErr.status >= 400 &&
225
+ apiErr.status < 500;
226
+ if (isNonRetryable) {
227
+ await updateCommandStatus(db, command.command_id, "failed", `permanent:${message}`);
228
+ syncLogger.warn("CommandQueueService", "Queued command rejected by server (not retrying)", {
229
+ command_id: command.command_id,
230
+ command_type: command.command_type,
231
+ status: apiErr.status,
232
+ error: message,
233
+ });
234
+ // Do not throw — allow replay to continue to next command.
235
+ return;
236
+ }
191
237
  await updateCommandStatus(db, command.command_id, "failed", message);
192
238
  syncLogger.command(parsedCommand, "failed", { error: message });
193
239
  throw error;
@@ -218,8 +264,17 @@ class CommandQueueService {
218
264
  try {
219
265
  await this.processCommand(command);
220
266
  }
221
- catch {
222
- // Stop replay on first failure; retry will happen on next trigger.
267
+ catch (e) {
268
+ const apiErr = e;
269
+ const isNonRetryable = apiErr?.name === "ApiError" &&
270
+ typeof apiErr.status === "number" &&
271
+ apiErr.status >= 400 &&
272
+ apiErr.status < 500;
273
+ if (isNonRetryable) {
274
+ // Already marked permanent in processCommand — continue to next.
275
+ continue;
276
+ }
277
+ // Network/5xx — stop to preserve ordering; retry later.
223
278
  break;
224
279
  }
225
280
  }
@@ -22,6 +22,13 @@ declare class OrderSnapshotsService {
22
22
  * duplicates).
23
23
  */
24
24
  persistOrderEvent(event: DomainEvent, origin: "local" | "lan" | "cloud"): Promise<boolean>;
25
+ /**
26
+ * Patch the latest status for a queued transition (e.g. accept_queued)
27
+ * carrying to_status/order_id. Used so offline devices see the new status
28
+ * immediately and after restart via the durable snapshot.
29
+ */
30
+ applyQueuedStatusEvent(event: DomainEvent, origin?: "local" | "lan" | "cloud"): Promise<void>;
31
+ private applyOrderStatusToReadModelInternal;
25
32
  /**
26
33
  * Patch the latest status of a known order inside every list snapshot for the
27
34
  * business, so offline devices see live status changes without a refetch.
@@ -50,17 +50,29 @@ class OrderSnapshotsService {
50
50
  }
51
51
  }
52
52
  /**
53
- * Patch the latest status of a known order inside every list snapshot for the
54
- * business, so offline devices see live status changes without a refetch.
53
+ * Patch the latest status for a queued transition (e.g. accept_queued)
54
+ * carrying to_status/order_id. Used so offline devices see the new status
55
+ * immediately and after restart via the durable snapshot.
55
56
  */
56
- async applyOrderStatusToReadModel(db, event) {
57
+ async applyQueuedStatusEvent(event, origin = "local") {
57
58
  const payload = event.payload;
58
- const orderId = payload?.order_id;
59
- const toStatus = payload?.to_status ??
60
- payload?.status;
61
- if (!orderId || !toStatus || event.event_type !== "order.status_changed") {
59
+ const toStatus = payload?.to_status;
60
+ const orderId = payload?.order_id ?? event.entity_id;
61
+ if (!toStatus || !orderId)
62
62
  return;
63
+ try {
64
+ const db = await openSyncDatabase();
65
+ await insertEvent(db, { event, origin });
66
+ await this.applyOrderStatusToReadModelInternal(db, orderId, toStatus, event);
67
+ }
68
+ catch (error) {
69
+ syncLogger.error("OrderSnapshots", "Failed to apply queued status", {
70
+ error: String(error),
71
+ event_type: event.event_type,
72
+ });
63
73
  }
74
+ }
75
+ async applyOrderStatusToReadModelInternal(db, orderId, toStatus, event) {
64
76
  try {
65
77
  const lists = await getEntitySnapshotsByType(db, "seller_orders");
66
78
  for (const list of lists) {
@@ -85,6 +97,20 @@ class OrderSnapshotsService {
85
97
  });
86
98
  }
87
99
  }
100
+ /**
101
+ * Patch the latest status of a known order inside every list snapshot for the
102
+ * business, so offline devices see live status changes without a refetch.
103
+ */
104
+ async applyOrderStatusToReadModel(db, event) {
105
+ const payload = event.payload;
106
+ const orderId = payload?.order_id;
107
+ const toStatus = payload?.to_status ??
108
+ payload?.status;
109
+ if (!orderId || !toStatus || event.event_type !== "order.status_changed") {
110
+ return;
111
+ }
112
+ await this.applyOrderStatusToReadModelInternal(db, orderId, toStatus, event);
113
+ }
88
114
  /**
89
115
  * Write through a fetched order list so it is available offline. Used by the
90
116
  * seller and POS order queries. `scope` distinguishes complementary lists
@@ -26,6 +26,16 @@ const ORDER_STATE_EVENT_TYPES = new Set([
26
26
  "delivery.status_changed",
27
27
  "delivery.assigned",
28
28
  ]);
29
+ const QUEUED_STATUS_EVENT_TYPES = new Set([
30
+ "order.accept_queued",
31
+ "order.reject_queued",
32
+ "order.start_preparing_queued",
33
+ "order.mark_ready_queued",
34
+ "order.complete_pickup_queued",
35
+ "order.cancel_queued",
36
+ "delivery.assign_queued",
37
+ "delivery.confirm_handover_queued",
38
+ ]);
29
39
  let invalidator = null;
30
40
  /**
31
41
  * Register a callback used to invalidate TanStack Query caches when a pending
@@ -88,7 +98,8 @@ export async function hydratePendingOrders() {
88
98
  */
89
99
  export async function applyOrderStateChangeEvent(event, origin = "local") {
90
100
  try {
91
- if (!ORDER_STATE_EVENT_TYPES.has(event.event_type))
101
+ const isQueued = QUEUED_STATUS_EVENT_TYPES.has(event.event_type);
102
+ if (!ORDER_STATE_EVENT_TYPES.has(event.event_type) && !isQueued)
92
103
  return;
93
104
  syncLogger.debug("OrderState", "Order state changed", {
94
105
  origin,
@@ -97,6 +108,12 @@ export async function applyOrderStateChangeEvent(event, origin = "local") {
97
108
  });
98
109
  // Persist the event into the durable journal and patch the offline read
99
110
  // model so every terminal keeps the same order states after a restart.
111
+ // Queued transitions carry to_status/order_id so they patch instantly offline.
112
+ if (isQueued) {
113
+ await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
114
+ invalidator?.();
115
+ return;
116
+ }
100
117
  await orderSnapshotsService.persistOrderEvent(event, origin);
101
118
  invalidator?.();
102
119
  }
@@ -118,6 +135,14 @@ export async function applyPendingOrderEvent(event, origin = "local") {
118
135
  await insertEvent(db, { event, origin });
119
136
  return;
120
137
  }
138
+ if (QUEUED_STATUS_EVENT_TYPES.has(event.event_type)) {
139
+ // Queued transitions: patch the read model so listing reflects new status offline.
140
+ await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
141
+ const db = await openSyncDatabase();
142
+ await insertEvent(db, { event, origin });
143
+ invalidator?.();
144
+ return;
145
+ }
121
146
  if (event.event_type === "order.created") {
122
147
  const payload = event.payload;
123
148
  const localOrderNumber = payload?.local_order_number;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Customer display helpers — offline-first, shared by mobile + desktop.
3
+ * SellerOrder shape comes from backend order_serializer (customer_name, is_pos, user{full_name, avatar_url, phone}).
4
+ */
5
+ import type { SellerOrder } from "../types/api/seller.api";
6
+ export type CustomerDisplay = {
7
+ name: string;
8
+ avatarUrl: string | null;
9
+ phone: string | null;
10
+ isWalkIn: boolean;
11
+ tellerName?: string | null;
12
+ };
13
+ type OrderLike = Pick<SellerOrder, "customer_name"> & {
14
+ user?: {
15
+ full_name?: string;
16
+ avatar_url?: string;
17
+ phone?: string;
18
+ } | null;
19
+ customer_phone?: string | null;
20
+ is_pos?: boolean;
21
+ };
22
+ /**
23
+ * Single source of truth for customer identity.
24
+ * - is_pos:true (POS walk-in): customer is walk-in/typed name, never the staff user.
25
+ * Staff is the teller (order.user) — do not show staff avatar as customer.
26
+ * Returns tellerName so UI can optionally show "Teller: <name>" underneath.
27
+ * - is_pos:false (WhatsApp / online / AI agent): customer_name || user.full_name || "Customer" + avatar from user.avatar_url
28
+ */
29
+ export declare function getCustomerDisplay(order: OrderLike): CustomerDisplay;
30
+ export declare function getCustomerSearchKeys(order: OrderLike): string[];
31
+ export declare function matchesCustomerSearch(order: OrderLike, qLower: string): boolean;
32
+ export {};
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Customer display helpers — offline-first, shared by mobile + desktop.
3
+ * SellerOrder shape comes from backend order_serializer (customer_name, is_pos, user{full_name, avatar_url, phone}).
4
+ */
5
+ /**
6
+ * Single source of truth for customer identity.
7
+ * - is_pos:true (POS walk-in): customer is walk-in/typed name, never the staff user.
8
+ * Staff is the teller (order.user) — do not show staff avatar as customer.
9
+ * Returns tellerName so UI can optionally show "Teller: <name>" underneath.
10
+ * - is_pos:false (WhatsApp / online / AI agent): customer_name || user.full_name || "Customer" + avatar from user.avatar_url
11
+ */
12
+ export function getCustomerDisplay(order) {
13
+ if (order.is_pos) {
14
+ const walkName = order.customer_name?.trim() ? order.customer_name.trim() : "Walk-in";
15
+ return {
16
+ name: walkName,
17
+ avatarUrl: null,
18
+ phone: order.customer_phone ?? null,
19
+ isWalkIn: true,
20
+ tellerName: order.user?.full_name?.trim() ?? null,
21
+ };
22
+ }
23
+ const rawName = order.customer_name?.trim()
24
+ ? order.customer_name.trim()
25
+ : order.user?.full_name?.trim() ?? "";
26
+ if (rawName) {
27
+ return {
28
+ name: rawName,
29
+ avatarUrl: order.user?.avatar_url ?? null,
30
+ phone: order.customer_phone ?? order.user?.phone ?? null,
31
+ isWalkIn: false,
32
+ };
33
+ }
34
+ return {
35
+ name: "Customer",
36
+ avatarUrl: order.user?.avatar_url ?? null,
37
+ phone: order.customer_phone ?? order.user?.phone ?? null,
38
+ isWalkIn: false,
39
+ };
40
+ }
41
+ export function getCustomerSearchKeys(order) {
42
+ return [
43
+ order.customer_name ?? "",
44
+ order.user?.full_name ?? "",
45
+ order.customer_phone ?? "",
46
+ order.user?.phone ?? "",
47
+ order.user?.avatar_url ?? "",
48
+ ];
49
+ }
50
+ export function matchesCustomerSearch(order, qLower) {
51
+ if (!qLower)
52
+ return true;
53
+ const hay = `${order.customer_name ?? ""} ${order.user?.full_name ?? ""} ${order.customer_phone ?? ""} ${order.user?.phone ?? ""}`.toLowerCase();
54
+ return hay.includes(qLower);
55
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Offline order list helpers — shared filtering + pagination for
3
+ * usePOSOrdersQuery / useSellerOrdersQuery snapshot fallbacks.
4
+ */
5
+ import type { SellerOrder } from "../types/api/seller.api";
6
+ export declare function filterOrdersOffline(orders: SellerOrder[], filters?: {
7
+ status?: string;
8
+ order_type?: string;
9
+ is_pos?: string;
10
+ q?: string;
11
+ search?: string;
12
+ }): SellerOrder[];
13
+ export declare function filterSellerOrdersOffline(orders: SellerOrder[], filters?: {
14
+ status?: string;
15
+ q?: string;
16
+ }): SellerOrder[];
17
+ export declare function paginateOffline<T>(orders: T[], page: number, pageSize?: number): {
18
+ data: T[];
19
+ pagination: {
20
+ page: number;
21
+ page_size: number;
22
+ total_pages: number;
23
+ total_count: number;
24
+ };
25
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Offline order list helpers — shared filtering + pagination for
3
+ * usePOSOrdersQuery / useSellerOrdersQuery snapshot fallbacks.
4
+ */
5
+ import { matchesCustomerSearch } from "./customer-display";
6
+ export function filterOrdersOffline(orders, filters) {
7
+ let filtered = orders;
8
+ const statusSet = filters?.status ? new Set(filters.status.split(",").map((s) => s.trim())) : null;
9
+ if (statusSet)
10
+ filtered = filtered.filter((o) => statusSet.has(o.status));
11
+ if (filters?.order_type)
12
+ filtered = filtered.filter((o) => (o.order_type ?? "pos") === filters.order_type);
13
+ if (filters?.is_pos !== undefined) {
14
+ const wantPos = filters.is_pos === "true" || filters.is_pos === "1";
15
+ filtered = filtered.filter((o) => !!o.is_pos === wantPos);
16
+ }
17
+ const q = (filters?.q ?? filters?.search ?? "").toLowerCase();
18
+ if (q) {
19
+ filtered = filtered.filter((o) => o.order_number.toLowerCase().includes(q) ||
20
+ matchesCustomerSearch(o, q));
21
+ }
22
+ return filtered;
23
+ }
24
+ export function filterSellerOrdersOffline(orders, filters) {
25
+ let filtered = orders;
26
+ if (filters?.status) {
27
+ const set = new Set(filters.status.split(",").map((s) => s.trim()));
28
+ filtered = filtered.filter((o) => set.has(o.status));
29
+ }
30
+ if (filters?.q) {
31
+ const q = filters.q.toLowerCase();
32
+ filtered = filtered.filter((o) => o.order_number.toLowerCase().includes(q) || matchesCustomerSearch(o, q));
33
+ }
34
+ return filtered;
35
+ }
36
+ export function paginateOffline(orders, page, pageSize = 20) {
37
+ const start = (page - 1) * pageSize;
38
+ const slice = orders.slice(start, start + pageSize);
39
+ const totalPages = Math.max(1, Math.ceil(orders.length / pageSize));
40
+ return { data: slice, pagination: { page, page_size: pageSize, total_pages: totalPages, total_count: orders.length } };
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "TeincFood shared offline-first core — types, sync engine, local DB, reference data, and repositories for mobile + desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",