@teincfood/core 0.7.7 → 0.7.9

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.
Files changed (48) hide show
  1. package/dist/adapters/auth.d.ts +2 -0
  2. package/dist/adapters/auth.js +7 -0
  3. package/dist/adapters/filesystem.d.ts +22 -0
  4. package/dist/adapters/filesystem.js +6 -0
  5. package/dist/adapters/print.d.ts +29 -0
  6. package/dist/adapters/print.js +12 -0
  7. package/dist/adapters/services.d.ts +3 -0
  8. package/dist/adapters/services.js +3 -0
  9. package/dist/cart/hooks.d.ts +36 -0
  10. package/dist/cart/hooks.js +45 -0
  11. package/dist/cart/store.d.ts +33 -0
  12. package/dist/cart/store.js +122 -0
  13. package/dist/catalog/hooks.d.ts +25 -0
  14. package/dist/catalog/hooks.js +160 -0
  15. package/dist/hooks/orders.hooks.d.ts +17 -0
  16. package/dist/hooks/orders.hooks.js +117 -0
  17. package/dist/image-cache/service.d.ts +29 -0
  18. package/dist/image-cache/service.js +125 -0
  19. package/dist/index.d.ts +24 -2
  20. package/dist/index.js +33 -2
  21. package/dist/printer/hooks.d.ts +32 -0
  22. package/dist/printer/hooks.js +53 -0
  23. package/dist/printer/receipt.d.ts +12 -0
  24. package/dist/printer/receipt.js +315 -0
  25. package/dist/printer/service.d.ts +31 -0
  26. package/dist/printer/service.js +119 -0
  27. package/dist/printer/store.d.ts +24 -0
  28. package/dist/printer/store.js +94 -0
  29. package/dist/printer/types.d.ts +12 -0
  30. package/dist/printer/types.js +1 -0
  31. package/dist/realtime/events.service.d.ts +18 -0
  32. package/dist/realtime/events.service.js +76 -0
  33. package/dist/realtime/order-events.service.d.ts +6 -0
  34. package/dist/realtime/order-events.service.js +17 -0
  35. package/dist/realtime/phoenix-socket.d.ts +55 -0
  36. package/dist/realtime/phoenix-socket.js +240 -0
  37. package/dist/realtime/useEventsConnection.d.ts +1 -0
  38. package/dist/realtime/useEventsConnection.js +41 -0
  39. package/dist/reference/capabilities.js +1 -0
  40. package/dist/repositories/order.repository.d.ts +1 -0
  41. package/dist/repositories/order.repository.js +9 -0
  42. package/dist/sync/command-queue.service.js +34 -2
  43. package/dist/sync/order-snapshots.service.d.ts +1 -0
  44. package/dist/sync/order-snapshots.service.js +30 -0
  45. package/dist/sync/pending-orders.service.js +81 -2
  46. package/dist/sync/transport-rest.js +12 -5
  47. package/dist/sync/use-sync-reconciliation.js +43 -15
  48. package/package.json +1 -1
@@ -272,6 +272,52 @@ 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
+ const pendingOrders = useCorePendingOrders(businessId);
281
+ return useQuery({
282
+ queryKey: SELLER_KEYS.order(orderId, businessId),
283
+ queryFn: async () => {
284
+ const pendingHit = pendingOrders.find((o) => o.id === orderId || o.order_number === orderId);
285
+ if (pendingHit)
286
+ return { data: pendingHit };
287
+ if (offline) {
288
+ const list = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
289
+ const found = list?.find((o) => o.id === orderId || o.order_number === orderId);
290
+ if (found)
291
+ return { data: found };
292
+ }
293
+ try {
294
+ const res = await sellerService.fetchSellerOrder(orderId);
295
+ return res;
296
+ }
297
+ catch (e) {
298
+ const list = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
299
+ const found = list?.find((o) => o.id === orderId || o.order_number === orderId);
300
+ if (found)
301
+ return { data: found };
302
+ const pendingAgain = getPendingOrdersSnapshot().orders.find((o) => o.id === orderId || o.order_number === orderId);
303
+ if (pendingAgain)
304
+ return { data: pendingAgain };
305
+ throw e;
306
+ }
307
+ },
308
+ enabled: enabled && !!orderId && !!businessId,
309
+ staleTime: 30 * 1000,
310
+ retry: false,
311
+ networkMode: "offlineFirst",
312
+ });
313
+ }
314
+ // ─── Helpers for queued UI ───────────────────────────────────────────────
315
+ export function isQueuedTransition(result) {
316
+ const r = result;
317
+ if (!r)
318
+ return false;
319
+ return r.status === "pending" && !!r.command_id && !r.id;
320
+ }
275
321
  // ─── Order status mutations via SyncEngine (all go through outbox) ────────
276
322
  function makeOrderActionMutation(build) {
277
323
  return function useOrderActionMutation() {
@@ -298,3 +344,74 @@ export const useStartPreparingOrderMutation = makeOrderActionMutation(({ orderId
298
344
  export const useMarkOrderReadyMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.markOrderReady({ orderId, businessId }));
299
345
  export const useCompletePickupMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.completePickup({ orderId, businessId }));
300
346
  export const useCancelOrderMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.cancelOrder({ orderId, businessId }));
347
+ export function useConfirmHandoverMutation() {
348
+ const businessId = useBusinessId();
349
+ const queryClient = useQueryClient();
350
+ return useMutation({
351
+ mutationFn: (deliveryId) => {
352
+ if (!businessId)
353
+ throw new Error("No active business");
354
+ return orderRepository.confirmHandover({ deliveryId, businessId });
355
+ },
356
+ onSettled: (_d, _e, deliveryId) => {
357
+ queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
358
+ queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
359
+ if (deliveryId)
360
+ queryClient.invalidateQueries({ queryKey: SELLER_KEYS.order(deliveryId) });
361
+ },
362
+ });
363
+ }
364
+ export function useAssignRiderMutation() {
365
+ const businessId = useBusinessId();
366
+ const queryClient = useQueryClient();
367
+ return useMutation({
368
+ mutationFn: ({ orderId, riderId }) => {
369
+ if (!businessId)
370
+ throw new Error("No active business");
371
+ return orderRepository.assignRider({ orderId, riderId, businessId });
372
+ },
373
+ onSettled: () => {
374
+ queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
375
+ queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
376
+ },
377
+ });
378
+ }
379
+ export function useConfirmCashPaymentMutation() {
380
+ const businessId = useBusinessId();
381
+ const queryClient = useQueryClient();
382
+ return useMutation({
383
+ mutationFn: (orderId) => {
384
+ if (!businessId)
385
+ throw new Error("No active business");
386
+ return orderRepository.confirmCashPayment({ orderId, businessId });
387
+ },
388
+ onSettled: (_d, _e, orderId) => {
389
+ queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
390
+ queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
391
+ if (orderId)
392
+ queryClient.invalidateQueries({ queryKey: SELLER_KEYS.order(orderId) });
393
+ },
394
+ });
395
+ }
396
+ export function useUpdateOrderStatusMutation() {
397
+ const accept = useAcceptOrderMutation();
398
+ const reject = useRejectOrderMutation();
399
+ const preparing = useStartPreparingOrderMutation();
400
+ const ready = useMarkOrderReadyMutation();
401
+ const complete = useCompletePickupMutation();
402
+ const cancel = useCancelOrderMutation();
403
+ return {
404
+ mutate: (orderId, status) => {
405
+ switch (status) {
406
+ case "accepted": return accept.mutateAsync(orderId);
407
+ case "preparing": return preparing.mutateAsync(orderId);
408
+ case "ready": return ready.mutateAsync(orderId);
409
+ case "delivered": return complete.mutateAsync(orderId);
410
+ case "cancelled": return cancel.mutateAsync(orderId);
411
+ case "rejected": return reject.mutateAsync(orderId);
412
+ default: throw new Error(`Unknown status ${status}`);
413
+ }
414
+ },
415
+ isPending: accept.isPending || reject.isPending || preparing.isPending || ready.isPending || complete.isPending || cancel.isPending,
416
+ };
417
+ }
@@ -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>;
@@ -0,0 +1,125 @@
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
+ import { getFileSystemAdapter } from "../adapters/filesystem";
7
+ import { syncLogger } from "../utils/sync-logger";
8
+ export function stableCacheUrl(url) {
9
+ try {
10
+ const p = new URL(url);
11
+ return `${p.origin}${p.pathname}`;
12
+ }
13
+ catch {
14
+ return url;
15
+ }
16
+ }
17
+ function cacheKeyFromUrl(url) {
18
+ let hash = 0;
19
+ const stable = stableCacheUrl(url);
20
+ for (let i = 0; i < stable.length; i++) {
21
+ hash = (hash << 5) - hash + stable.charCodeAt(i);
22
+ hash |= 0;
23
+ }
24
+ return `${Math.abs(hash).toString(16)}.img`;
25
+ }
26
+ function getImageCacheDir(fs) {
27
+ const base = fs?.getDocumentDirectory() ?? "";
28
+ return `${base}teincfood_images/`;
29
+ }
30
+ export function getCachedImagePath(url) {
31
+ const fs = getFileSystemAdapter();
32
+ return `${getImageCacheDir(fs)}${cacheKeyFromUrl(url)}`;
33
+ }
34
+ export async function ensureImageCacheDir() {
35
+ const fs = getFileSystemAdapter();
36
+ if (!fs)
37
+ return "";
38
+ const dir = getImageCacheDir(fs);
39
+ const info = await fs.getInfoAsync(dir);
40
+ if (!info.exists)
41
+ await fs.makeDirectoryAsync(dir, { intermediates: true });
42
+ return dir;
43
+ }
44
+ export async function getCachedImageInfo(url) {
45
+ const fs = getFileSystemAdapter();
46
+ if (!fs)
47
+ return { remoteUrl: url, localUri: url, etag: null, lastModified: null, cachedAt: new Date().toISOString() };
48
+ const localPath = getCachedImagePath(url);
49
+ const info = await fs.getInfoAsync(localPath);
50
+ return { remoteUrl: url, localUri: info.exists && !info.isDirectory ? localPath : null, etag: null, lastModified: null, cachedAt: info.exists ? new Date().toISOString() : null };
51
+ }
52
+ export async function cacheRemoteImage(url, opts = {}) {
53
+ if (!url)
54
+ return null;
55
+ const fs = getFileSystemAdapter();
56
+ if (!fs)
57
+ return { remoteUrl: url, localUri: url, etag: null, lastModified: null, cachedAt: new Date().toISOString() };
58
+ await ensureImageCacheDir();
59
+ const localPath = getCachedImagePath(url);
60
+ const existing = await fs.getInfoAsync(localPath);
61
+ if (opts.skipIfCached && existing.exists && !existing.isDirectory) {
62
+ return { remoteUrl: url, localUri: localPath, etag: null, lastModified: null, cachedAt: new Date().toISOString() };
63
+ }
64
+ try {
65
+ const res = await fs.downloadAsync(url, localPath);
66
+ if (res.uri)
67
+ return { remoteUrl: url, localUri: localPath, etag: null, lastModified: null, cachedAt: new Date().toISOString() };
68
+ }
69
+ catch (e) {
70
+ syncLogger.warn("ImageCache", "Failed to download", { url, error: String(e) });
71
+ }
72
+ if (opts.removeOnFailure)
73
+ await fs.deleteAsync(localPath, { idempotent: true }).catch(() => { });
74
+ if (existing.exists && !existing.isDirectory)
75
+ return { remoteUrl: url, localUri: localPath, etag: null, lastModified: null, cachedAt: new Date().toISOString() };
76
+ return { remoteUrl: url, localUri: null, etag: null, lastModified: null, cachedAt: null };
77
+ }
78
+ export async function cacheImageBatch(urls, opts = {}) {
79
+ const results = await Promise.all(urls.map((u) => cacheRemoteImage(u, opts)));
80
+ return results.filter((r) => r !== null);
81
+ }
82
+ export async function clearImageCache() {
83
+ const fs = getFileSystemAdapter();
84
+ if (!fs)
85
+ return;
86
+ await fs.deleteAsync(getImageCacheDir(fs), { idempotent: true });
87
+ }
88
+ export async function pruneImageCache(knownUrls, maxAgeDays = 30) {
89
+ try {
90
+ const fs = getFileSystemAdapter();
91
+ if (!fs)
92
+ return 0;
93
+ const known = new Set();
94
+ for (const u of knownUrls)
95
+ if (u)
96
+ known.add(u);
97
+ const dir = getImageCacheDir(fs);
98
+ const info = await fs.getInfoAsync(dir);
99
+ if (!info.exists || info.isDirectory !== true)
100
+ return 0;
101
+ // cutoff check skipped without modificationTime — just orphan check
102
+ const entries = await fs.readDirectoryAsync(dir);
103
+ let pruned = 0;
104
+ for (const f of entries) {
105
+ if (!f.endsWith(".img"))
106
+ continue;
107
+ if ([...known].some((url) => getCachedImagePath(url) === `${dir}${f}`))
108
+ continue;
109
+ await fs.deleteAsync(`${dir}${f}`, { idempotent: true });
110
+ pruned += 1;
111
+ }
112
+ if (pruned > 0)
113
+ syncLogger.info("ImageCache", "Pruned orphan images", { pruned });
114
+ return pruned;
115
+ }
116
+ catch {
117
+ return 0;
118
+ }
119
+ }
120
+ export async function resolveImageSource(url) {
121
+ if (!url)
122
+ return null;
123
+ const cached = await getCachedImageInfo(url);
124
+ return cached.localUri ? { uri: cached.localUri } : { uri: url };
125
+ }
package/dist/index.d.ts CHANGED
@@ -63,16 +63,32 @@ export * from "./reference/service";
63
63
  export { referenceSyncService } from "./reference/service";
64
64
  export * from "./reference/capabilities";
65
65
  export * from "./adapters/sqlite";
66
- export * from "./adapters/kv";
66
+ export { KVDriver, setKVDriver, getKVDriver, kvGetItem, kvSetItem, kvRemoveItem, getItem, setItem, removeItem, getItemSync, setItemSync } from "./adapters/kv";
67
67
  export * from "./adapters/connectivity";
68
68
  export * from "./adapters/http";
69
69
  export * from "./adapters/local-node";
70
70
  export * from "./adapters/services";
71
71
  export * from "./adapters/devices";
72
- export * from "./adapters/image-cache";
72
+ export { setAuthProvider, getAuthToken } from "./adapters/auth";
73
+ export { imageCacheService } from "./adapters/image-cache";
73
74
  export * from "./adapters/business";
74
75
  export * from "./adapters/pending-orders";
76
+ export * from "./adapters/print";
77
+ export * from "./adapters/filesystem";
75
78
  export { KIOSK_KEYS, SELLER_ORDERS_KEYS, BUSINESS_DELIVERIES_KEYS, MENU_KEYS, REFERENCE_KEYS } from "./adapters/query-keys";
79
+ export * from "./printer/types";
80
+ export * from "./printer/store";
81
+ export * from "./printer/receipt";
82
+ export * from "./printer/service";
83
+ export * from "./printer/hooks";
84
+ export * from "./cart/store";
85
+ export * from "./cart/hooks";
86
+ export * from "./catalog/hooks";
87
+ export { phoenixSocket } from "./realtime/phoenix-socket";
88
+ export { eventsService } from "./realtime/events.service";
89
+ export { orderEventsService } from "./realtime/order-events.service";
90
+ export { useEventsConnection } from "./realtime/useEventsConnection";
91
+ export { getCachedImagePath, ensureImageCacheDir, getCachedImageInfo, cacheRemoteImage, cacheImageBatch, clearImageCache, pruneImageCache, resolveImageSource, stableCacheUrl } from "./image-cache/service";
76
92
  import { type SqliteDriver } from "./adapters/sqlite";
77
93
  import { type KVDriver } from "./adapters/kv";
78
94
  import { type HttpClient } from "./adapters/http";
@@ -88,6 +104,12 @@ export interface CoreConfig {
88
104
  isOnline?: boolean;
89
105
  isCloudReachable?: "reachable" | "unreachable" | "unknown";
90
106
  };
107
+ printAdapters?: {
108
+ tcp?: import("./adapters/print").TcpSocketAdapter;
109
+ printFile?: import("./adapters/print").PrintFileAdapter;
110
+ mdns?: import("./adapters/print").MdnsAdapter;
111
+ };
112
+ fileSystemAdapter?: import("./adapters/filesystem").FileSystemAdapter;
91
113
  }
92
114
  export declare function createTeincCore(config?: CoreConfig): {
93
115
  engine: SyncEngine;
package/dist/index.js CHANGED
@@ -66,22 +66,45 @@ export { referenceSyncService } from "./reference/service";
66
66
  export * from "./reference/capabilities";
67
67
  // ── Adapters (for host apps to supply) ──
68
68
  export * from "./adapters/sqlite";
69
- export * from "./adapters/kv";
69
+ export { setKVDriver, getKVDriver, kvGetItem, kvSetItem, kvRemoveItem, getItem, setItem, removeItem, getItemSync, setItemSync } from "./adapters/kv";
70
70
  export * from "./adapters/connectivity";
71
71
  export * from "./adapters/http";
72
72
  export * from "./adapters/local-node";
73
73
  export * from "./adapters/services";
74
74
  export * from "./adapters/devices";
75
- export * from "./adapters/image-cache";
75
+ export { setAuthProvider, getAuthToken } from "./adapters/auth";
76
+ export { imageCacheService } from "./adapters/image-cache";
76
77
  export * from "./adapters/business";
77
78
  export * from "./adapters/pending-orders";
79
+ export * from "./adapters/print";
80
+ export * from "./adapters/filesystem";
78
81
  export { KIOSK_KEYS, SELLER_ORDERS_KEYS, BUSINESS_DELIVERIES_KEYS, MENU_KEYS, REFERENCE_KEYS } from "./adapters/query-keys";
79
82
  // POS_KEYS / SELLER_KEYS are exported from hooks/orders.hooks (offline-first source of truth)
83
+ // ── Printer (backend-identical template) ──
84
+ export * from "./printer/types";
85
+ export * from "./printer/store";
86
+ export * from "./printer/receipt";
87
+ export * from "./printer/service";
88
+ export * from "./printer/hooks";
89
+ // ── Cart (device-local) ──
90
+ export * from "./cart/store";
91
+ export * from "./cart/hooks";
92
+ // ── Catalog ──
93
+ export * from "./catalog/hooks";
94
+ // ── Realtime (single source — PHX WebSocket + business:{id}) ──
95
+ export { phoenixSocket } from "./realtime/phoenix-socket";
96
+ export { eventsService } from "./realtime/events.service";
97
+ export { orderEventsService } from "./realtime/order-events.service";
98
+ export { useEventsConnection } from "./realtime/useEventsConnection";
99
+ // ── Image cache (durable) ──
100
+ export { getCachedImagePath, ensureImageCacheDir, getCachedImageInfo, cacheRemoteImage, cacheImageBatch, clearImageCache, pruneImageCache, resolveImageSource, stableCacheUrl } from "./image-cache/service";
80
101
  // ── Init helper ──
81
102
  import { setSqliteDriver } from "./adapters/sqlite";
82
103
  import { setKVDriver } from "./adapters/kv";
83
104
  import { setConnectivityState } from "./adapters/connectivity";
84
105
  import { setHttpClient } from "./adapters/http";
106
+ import { setTcpAdapter, setPrintFileAdapter, setMdnsAdapter } from "./adapters/print";
107
+ import { setFileSystemAdapter } from "./adapters/filesystem";
85
108
  import { API_BASE_URL, WS_BASE_URL, setApiBaseUrl, setWsBaseUrl } from "./utils/constants";
86
109
  import { SyncEngine } from "./sync/engine";
87
110
  import { restTransport } from "./sync/transport-rest";
@@ -105,6 +128,14 @@ export function createTeincCore(config = {}) {
105
128
  setKVDriver(config.kvDriver);
106
129
  if (config.httpClient)
107
130
  setHttpClient(config.httpClient);
131
+ if (config.printAdapters?.tcp)
132
+ setTcpAdapter(config.printAdapters.tcp);
133
+ if (config.printAdapters?.printFile)
134
+ setPrintFileAdapter(config.printAdapters.printFile);
135
+ if (config.printAdapters?.mdns)
136
+ setMdnsAdapter(config.printAdapters.mdns);
137
+ if (config.fileSystemAdapter)
138
+ setFileSystemAdapter(config.fileSystemAdapter);
108
139
  if (config.apiBaseUrl)
109
140
  setApiBaseUrl(config.apiBaseUrl);
110
141
  else {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Printer hooks for UI.
3
+ */
4
+ import { setDefaultPrinter, setAutoPrint, addPrinter, updatePrinter, removePrinter } from "./store";
5
+ import type { SellerOrder } from "../types/api/seller.api";
6
+ export declare function usePrinter(): {
7
+ defaultPrinterId: string | null;
8
+ printers: import("./types").PrinterConfig[];
9
+ autoPrint: boolean;
10
+ setDefaultPrinter: typeof setDefaultPrinter;
11
+ setAutoPrint: typeof setAutoPrint;
12
+ addPrinter: typeof addPrinter;
13
+ updatePrinter: typeof updatePrinter;
14
+ removePrinter: typeof removePrinter;
15
+ };
16
+ export declare function usePrintOrder(): {
17
+ print: (order: SellerOrder) => Promise<void>;
18
+ printing: boolean;
19
+ };
20
+ export declare function useDiscoverPrinters(): {
21
+ printers: {
22
+ name: string;
23
+ ip: string;
24
+ port: number;
25
+ }[];
26
+ discovering: boolean;
27
+ discover: (timeoutMs?: number) => Promise<{
28
+ name: string;
29
+ ip: string;
30
+ port: number;
31
+ }[]>;
32
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Printer hooks for UI.
3
+ */
4
+ import { useSyncExternalStore, useCallback } from "react";
5
+ import { subscribePrinter, getPrinterSnapshot, setDefaultPrinter, setAutoPrint, addPrinter, updatePrinter, removePrinter, hydratePrinterStore } from "./store";
6
+ import { getMdnsAdapter } from "../adapters/print";
7
+ import { printerService } from "./service";
8
+ import { useEffect, useState } from "react";
9
+ export function usePrinter() {
10
+ const state = useSyncExternalStore(subscribePrinter, getPrinterSnapshot, getPrinterSnapshot);
11
+ useEffect(() => { void hydratePrinterStore(); }, []);
12
+ return {
13
+ ...state,
14
+ setDefaultPrinter,
15
+ setAutoPrint,
16
+ addPrinter,
17
+ updatePrinter,
18
+ removePrinter,
19
+ };
20
+ }
21
+ export function usePrintOrder() {
22
+ const [printing, setPrinting] = useState(false);
23
+ const print = useCallback(async (order) => {
24
+ setPrinting(true);
25
+ try {
26
+ await printerService.printOrderReceipt(order);
27
+ }
28
+ finally {
29
+ setPrinting(false);
30
+ }
31
+ }, []);
32
+ return { print, printing };
33
+ }
34
+ export function useDiscoverPrinters() {
35
+ const [discovering, setDiscovering] = useState(false);
36
+ const [printers, setPrinters] = useState([]);
37
+ const discover = useCallback(async (timeoutMs = 8000) => {
38
+ setDiscovering(true);
39
+ try {
40
+ const mdns = getMdnsAdapter();
41
+ if (mdns?.discoverPrinters) {
42
+ const found = await mdns.discoverPrinters(timeoutMs);
43
+ setPrinters(found);
44
+ return found;
45
+ }
46
+ return [];
47
+ }
48
+ finally {
49
+ setDiscovering(false);
50
+ }
51
+ }, []);
52
+ return { printers, discovering, discover };
53
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Local receipt generation — 1:1 port of TeincFoodBackend.Orders.Receipt
3
+ * Generates ESC/POS (80mm, 32 chars) and HTML from a SellerOrder snapshot.
4
+ * Works fully offline for queued DD-RRR-CCC orders without backend fetch.
5
+ *
6
+ * Backend source: lib/teinc_food_backend/orders/receipt.ex
7
+ * Keep this file in sync with backend when template changes.
8
+ */
9
+ import type { SellerOrder } from "../types/api/seller.api";
10
+ export declare function calloutNumber(order: SellerOrder): string;
11
+ export declare function generateEscpos(order: SellerOrder): string;
12
+ export declare function generateHtml(order: SellerOrder, logoDataUri?: string | null): string;