@tallyui/pos 3.4.0 → 3.5.0

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/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { Money, TaxRounding, StoreSettings, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettingsChoices, ProductTraits, OrderCreateLine, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, OrderCreateEnvelope, RegisterCommandType, RegisterCommandResult, VariantSummary, TraitContext, CommandResult, AnyCommandEnvelope, CommandEnvelope, OrderCreatePayload, RegisterCommandEnvelope } from '@tallyui/core';
3
+ import { Money, TaxRounding, StoreSettings, TallyConnector, SyncContext, StoreSettingsChoice, StoreSettingsChoices, ProductTraits, ProductCategory, TraitContext, OrderCreateLine, PaymentMethodKind, CommandServerRefs, CommandWarning, CommandError, ServerCapabilities, OrderCreateEnvelope, RegisterCommandType, RegisterCommandResult, VariantSummary, CommandResult, AnyCommandEnvelope, CommandEnvelope, OrderCreatePayload, RegisterCommandEnvelope } from '@tallyui/core';
4
4
  export { getProductStock, withStockOverlay } from '@tallyui/core';
5
5
  export { countFresh, readFresh, watchFresh } from '@tallyui/core/rxdb';
6
6
  import * as rxdb from 'rxdb';
@@ -618,6 +618,53 @@ declare function buildReceiptData(order: SentOrder, config: ReceiptConfig): Rece
618
618
  */
619
619
  declare function searchProducts<Doc>(docs: Doc[], term: string, traits: ProductTraits<Doc>): Doc[];
620
620
 
621
+ /** The product's categories: `getCategories`, else `getCategoryNames` with each name as its id. */
622
+ declare function productCategories<Doc>(doc: Doc, traits: ProductTraits<Doc>): ProductCategory[];
623
+ /** Every category across `docs`, once per id (the first name seen wins), sorted by name (natural, case-insensitive), ties by id. */
624
+ declare function listCategories<Doc>(docs: readonly Doc[], traits: ProductTraits<Doc>): ProductCategory[];
625
+ /** Whether `doc` is in the category with this id. */
626
+ declare function inCategory<Doc>(doc: Doc, categoryId: string, traits: ProductTraits<Doc>): boolean;
627
+
628
+ /** A sort on one field: ascending or descending. */
629
+ interface ProductSort {
630
+ field: string;
631
+ dir: 'asc' | 'desc';
632
+ }
633
+ type ProductSortValue = string | number | undefined;
634
+ /** The fields productSortValue knows, in the default column order. */
635
+ declare const PRODUCT_SORT_FIELDS: readonly ["name", "sku", "barcode", "price", "stock", "category"];
636
+ declare function productSortValue<Doc>(doc: Doc, field: string, traits: ProductTraits<Doc>, context?: TraitContext): ProductSortValue;
637
+ declare function sortProducts<Doc>(docs: Doc[], sort: ProductSort | null | undefined, valueOf: (doc: Doc, field: string) => ProductSortValue): Doc[];
638
+
639
+ type CatalogueView = 'grid' | 'table';
640
+ type CatalogueGridColumns = 'auto' | 2 | 3 | 4 | 5 | 6 | 7 | 8;
641
+ interface CatalogueViewState {
642
+ view: CatalogueView;
643
+ gridColumns: CatalogueGridColumns;
644
+ sort: ProductSort | null;
645
+ categoryId: string | null;
646
+ }
647
+ declare const DEFAULT_CATALOGUE_VIEW_STATE: CatalogueViewState;
648
+ type CatalogueViewAction = {
649
+ type: 'setView';
650
+ view: CatalogueView;
651
+ } | {
652
+ type: 'setGridColumns';
653
+ gridColumns: CatalogueGridColumns;
654
+ } | {
655
+ type: 'setSort';
656
+ sort: ProductSort | null;
657
+ } | {
658
+ type: 'setCategory';
659
+ categoryId: string | null;
660
+ } | {
661
+ type: 'replace';
662
+ state: CatalogueViewState;
663
+ };
664
+ declare function catalogueViewReducer(state: CatalogueViewState, action: CatalogueViewAction): CatalogueViewState;
665
+ declare function normalizeCatalogueViewState(raw: unknown, defaults?: CatalogueViewState): CatalogueViewState;
666
+ declare function resolveGridColumns(gridColumns: CatalogueGridColumns, containerWidth: number): number;
667
+
621
668
  /** The `stock_levels` collection as a live map of key to stock value. */
622
669
  declare function stockOverlay$(collection: RxCollection): Observable<Map<string, unknown>>;
623
670
  /**
@@ -2449,4 +2496,4 @@ declare function planLegs(plan: TenderPlan, rowsSinceFrom: {
2449
2496
  };
2450
2497
  };
2451
2498
 
2452
- export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type ChargeInput, type ChargeLine, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FeeLine, type FinalizeOptions, type HttpTransportOptions, type LedgerRow, type LineItem, type LineTaxLine, type LogEntry, type LogLevel, type LogSink, type Logger, MAX_TENDER_MINOR, MICROS_PER_MINOR, type Movement$1 as Movement, type MovementType, type Order, type OrderBuilder, type OrderBuilderOptions, OrderContentMismatchError, type OrderManager, type OrderManagerOptions, type OrderOutbox, type OrderOutboxOptions, type OrderTaxTotals, type OrderTransportContext, type OutboxState, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type ShippingLine, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, initTenderState, initialTenderState, isServerDecimal, labelKeys, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, observeRegister$, openSession, openSessionSelector, orderDraftSchema, outboxLogger, overThreshold, parkedOrderSummaries$, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveStoreSettings, restoreOrderDraft, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useParkedSales, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
2499
+ export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type CatalogueGridColumns, type CatalogueView, type CatalogueViewAction, type CatalogueViewState, type ChargeInput, type ChargeLine, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DEFAULT_CATALOGUE_VIEW_STATE, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FeeLine, type FinalizeOptions, type HttpTransportOptions, type LedgerRow, type LineItem, type LineTaxLine, type LogEntry, type LogLevel, type LogSink, type Logger, MAX_TENDER_MINOR, MICROS_PER_MINOR, type Movement$1 as Movement, type MovementType, type Order, type OrderBuilder, type OrderBuilderOptions, OrderContentMismatchError, type OrderManager, type OrderManagerOptions, type OrderOutbox, type OrderOutboxOptions, type OrderTaxTotals, type OrderTransportContext, type OutboxState, PRODUCT_SORT_FIELDS, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type ProductSort, type ProductSortValue, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type ShippingLine, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, catalogueViewReducer, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, inCategory, initTenderState, initialTenderState, isServerDecimal, labelKeys, listCategories, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, normalizeCatalogueViewState, observeRegister$, openSession, openSessionSelector, orderDraftSchema, outboxLogger, overThreshold, parkedOrderSummaries$, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, productCategories, productSortValue, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveGridColumns, resolveStoreSettings, restoreOrderDraft, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, sortProducts, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useParkedSales, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure, writeOrderDraft };
package/dist/index.js CHANGED
@@ -1454,6 +1454,110 @@ function searchProducts(docs, term, traits) {
1454
1454
  });
1455
1455
  }
1456
1456
 
1457
+ // src/product/categories.ts
1458
+ function productCategories(doc, traits) {
1459
+ return traits.getCategories ? traits.getCategories(doc) : traits.getCategoryNames(doc).map((name) => ({ id: name, name }));
1460
+ }
1461
+ function listCategories(docs, traits) {
1462
+ const categories = /* @__PURE__ */ new Map();
1463
+ for (const doc of docs) {
1464
+ for (const category of productCategories(doc, traits)) {
1465
+ if (!categories.has(category.id)) categories.set(category.id, category);
1466
+ }
1467
+ }
1468
+ return [...categories.values()].sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true, sensitivity: "base" }) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
1469
+ }
1470
+ function inCategory(doc, categoryId, traits) {
1471
+ return productCategories(doc, traits).some((category) => category.id === categoryId);
1472
+ }
1473
+
1474
+ // src/product/catalogue-view-state.ts
1475
+ var DEFAULT_CATALOGUE_VIEW_STATE = Object.freeze({
1476
+ view: "grid",
1477
+ gridColumns: "auto",
1478
+ sort: null,
1479
+ categoryId: null
1480
+ });
1481
+ function catalogueViewReducer(state, action) {
1482
+ switch (action.type) {
1483
+ case "setView":
1484
+ return state.view === action.view ? state : { ...state, view: action.view };
1485
+ case "setGridColumns":
1486
+ return state.gridColumns === action.gridColumns ? state : { ...state, gridColumns: action.gridColumns };
1487
+ case "setCategory":
1488
+ return state.categoryId === action.categoryId ? state : { ...state, categoryId: action.categoryId };
1489
+ case "setSort":
1490
+ return state.sort?.field === action.sort?.field && state.sort?.dir === action.sort?.dir ? state : { ...state, sort: action.sort };
1491
+ case "replace":
1492
+ return action.state;
1493
+ }
1494
+ }
1495
+ function normalizeCatalogueViewState(raw, defaults = DEFAULT_CATALOGUE_VIEW_STATE) {
1496
+ if (typeof raw !== "object" || raw === null) return defaults;
1497
+ try {
1498
+ const { view, gridColumns, sort, categoryId } = raw;
1499
+ const candidate = sort;
1500
+ return {
1501
+ categoryId: categoryId === null || typeof categoryId === "string" && categoryId.length > 0 ? categoryId : defaults.categoryId,
1502
+ view: view === "grid" || view === "table" ? view : defaults.view,
1503
+ gridColumns: gridColumns === "auto" || typeof gridColumns === "number" && Number.isInteger(gridColumns) && gridColumns >= 2 && gridColumns <= 8 ? gridColumns : defaults.gridColumns,
1504
+ sort: sort === null ? null : typeof candidate === "object" && candidate && typeof candidate.field === "string" && candidate.field.length > 0 && (candidate.dir === "asc" || candidate.dir === "desc") ? { field: candidate.field, dir: candidate.dir } : defaults.sort
1505
+ };
1506
+ } catch {
1507
+ return defaults;
1508
+ }
1509
+ }
1510
+ var MIN_TILE_WIDTH = 160;
1511
+ function resolveGridColumns(gridColumns, containerWidth) {
1512
+ return gridColumns === "auto" ? Math.max(2, Math.min(6, Math.floor((containerWidth - 8) / (MIN_TILE_WIDTH + 8)))) : gridColumns;
1513
+ }
1514
+
1515
+ // src/product/sort-products.ts
1516
+ import { resolvePrice as resolvePrice2, resolvePriceRange } from "@tallyui/core";
1517
+ var PRODUCT_SORT_FIELDS = ["name", "sku", "barcode", "price", "stock", "category"];
1518
+ function productSortValue(doc, field, traits, context) {
1519
+ let value;
1520
+ switch (field) {
1521
+ case "name":
1522
+ value = traits.getName(doc);
1523
+ break;
1524
+ case "sku":
1525
+ value = traits.getSku(doc);
1526
+ break;
1527
+ case "barcode":
1528
+ value = traits.getBarcode(doc);
1529
+ break;
1530
+ case "price": {
1531
+ const variants = traits.getVariants?.(doc, context);
1532
+ value = variants && variants.length >= 2 ? resolvePriceRange(variants, context?.currency)?.min.amount : resolvePrice2(traits.getPrices(doc, context), context?.currency)?.current.amount;
1533
+ break;
1534
+ }
1535
+ case "stock":
1536
+ value = traits.getStock(doc).quantity;
1537
+ break;
1538
+ case "category":
1539
+ value = traits.getCategoryNames(doc).join(", ");
1540
+ break;
1541
+ default:
1542
+ return void 0;
1543
+ }
1544
+ return value === "" ? void 0 : value;
1545
+ }
1546
+ function sortProducts(docs, sort, valueOf) {
1547
+ if (!sort) return docs;
1548
+ return docs.map((doc, index) => ({ doc, index, value: valueOf(doc, sort.field) })).sort((a, b) => {
1549
+ const aMissing = a.value === void 0 || a.value === "";
1550
+ const bMissing = b.value === void 0 || b.value === "";
1551
+ if (aMissing || bMissing) {
1552
+ return aMissing === bMissing ? a.index - b.index : aMissing ? 1 : -1;
1553
+ }
1554
+ const av = a.value;
1555
+ const bv = b.value;
1556
+ const comparison = typeof av === "number" ? typeof bv === "number" ? av - bv : -1 : typeof bv === "number" ? 1 : av.localeCompare(bv, void 0, { numeric: true, sensitivity: "base" });
1557
+ return (sort.dir === "desc" ? -comparison : comparison) || a.index - b.index;
1558
+ }).map(({ doc }) => doc);
1559
+ }
1560
+
1457
1561
  // src/product/stock.ts
1458
1562
  import { map as map3 } from "rxjs";
1459
1563
  import { STOCK_LEVELS_LAST_PASS } from "@tallyui/core";
@@ -3905,7 +4009,7 @@ function useRegisterSession(options) {
3905
4009
  }
3906
4010
 
3907
4011
  // src/sale/cart.ts
3908
- import { resolvePrice as resolvePrice2 } from "@tallyui/core";
4012
+ import { resolvePrice as resolvePrice3 } from "@tallyui/core";
3909
4013
  var CartError = class extends Error {
3910
4014
  };
3911
4015
  function addEntryToCart(builder, entry, traits, currency) {
@@ -3916,7 +4020,7 @@ function addEntryToCart(builder, entry, traits, currency) {
3916
4020
  return existing.id;
3917
4021
  }
3918
4022
  const name = traits.getName(product) + (traits.getVariantCount(product) > 1 ? ` \xB7 ${variant.title}` : "");
3919
- const unitPrice = resolvePrice2(variant.prices, currency)?.current;
4023
+ const unitPrice = resolvePrice3(variant.prices, currency)?.current;
3920
4024
  if (!unitPrice) throw new CartError(`No ${currency} price for ${name}`);
3921
4025
  return builder.addLine({
3922
4026
  productId: traits.getId(product),
@@ -4424,7 +4528,7 @@ function useSale(settings, opts) {
4424
4528
  }
4425
4529
 
4426
4530
  // src/sale/catalogue.ts
4427
- import { findVariantByCode, formatMoney as formatMoney2, resolvePrice as resolvePrice3 } from "@tallyui/core";
4531
+ import { findVariantByCode, formatMoney as formatMoney2, resolvePrice as resolvePrice4 } from "@tallyui/core";
4428
4532
  function catalogueEntries(products, traits, context) {
4429
4533
  return products.flatMap((product) => {
4430
4534
  const variants = traits.getVariants ? traits.getVariants(product, context) : traits.hasVariants(product) ? [] : [{
@@ -4442,7 +4546,7 @@ function findEntryByCode(entries, code) {
4442
4546
  return variant ? entries.find((entry) => entry.variant === variant) : void 0;
4443
4547
  }
4444
4548
  function variantPriceLabel(variant, currency, locale) {
4445
- const price = resolvePrice3(variant.prices, currency);
4549
+ const price = resolvePrice4(variant.prices, currency);
4446
4550
  return price ? formatMoney2(price.current, locale) : void 0;
4447
4551
  }
4448
4552
 
@@ -5606,10 +5710,12 @@ function planLegs(plan, rowsSinceFrom, balanceMinor) {
5606
5710
  export {
5607
5711
  CartError,
5608
5712
  CurrencyProvider,
5713
+ DEFAULT_CATALOGUE_VIEW_STATE,
5609
5714
  DISCOUNTS_UNSUPPORTED,
5610
5715
  MAX_TENDER_MINOR,
5611
5716
  MICROS_PER_MINOR,
5612
5717
  OrderContentMismatchError,
5718
+ PRODUCT_SORT_FIELDS,
5613
5719
  PosOrderOpenClosedError,
5614
5720
  RegisterApprovalRequiredError,
5615
5721
  RegisterCloseIncompleteError,
@@ -5639,6 +5745,7 @@ export {
5639
5745
  callbackSink,
5640
5746
  cashMovementSchema,
5641
5747
  catalogueEntries,
5748
+ catalogueViewReducer,
5642
5749
  changeMinor,
5643
5750
  clampClosureScope,
5644
5751
  closeNeedsApproval,
@@ -5670,10 +5777,12 @@ export {
5670
5777
  getBoundRegisterId,
5671
5778
  getDeviceId,
5672
5779
  getProductStock,
5780
+ inCategory,
5673
5781
  initTenderState,
5674
5782
  initialTenderState,
5675
5783
  isServerDecimal,
5676
5784
  labelKeys,
5785
+ listCategories,
5677
5786
  minorToDecimal,
5678
5787
  mintClosureNumber,
5679
5788
  mintUuid,
@@ -5682,6 +5791,7 @@ export {
5682
5791
  needsAttention,
5683
5792
  nextSaleCounter,
5684
5793
  normalizeAmount,
5794
+ normalizeCatalogueViewState,
5685
5795
  observeRegister$,
5686
5796
  openSession,
5687
5797
  openSessionSelector,
@@ -5694,6 +5804,8 @@ export {
5694
5804
  posOrderCollection,
5695
5805
  posOrderSchema,
5696
5806
  posOrdersLogger,
5807
+ productCategories,
5808
+ productSortValue,
5697
5809
  quickTenderedAmounts,
5698
5810
  ratePpmFromPercent,
5699
5811
  readBoundRegister,
@@ -5709,6 +5821,7 @@ export {
5709
5821
  registerSessionCollection,
5710
5822
  registerSessionSchema,
5711
5823
  requireOpenSession,
5824
+ resolveGridColumns,
5712
5825
  resolveStoreSettings,
5713
5826
  restoreOrderDraft,
5714
5827
  roundMicrosToMinor,
@@ -5718,6 +5831,7 @@ export {
5718
5831
  selectClosureRows,
5719
5832
  sessionOpenCommand,
5720
5833
  sessionTransitionCommand,
5834
+ sortProducts,
5721
5835
  stampSession,
5722
5836
  startCounting,
5723
5837
  stockOverlay$,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/pos",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "description": "Shared POS business logic for Tally UI — tax, orders, payments, discounts, receipts",
6
6
  "main": "./dist/index.js",
@@ -27,20 +27,20 @@
27
27
  "directory": "packages/pos"
28
28
  },
29
29
  "peerDependencies": {
30
- "@tallyui/core": "^3.4.0",
30
+ "@tallyui/core": "^3.5.0",
31
31
  "react": ">=18",
32
32
  "rxdb": "~17.5.0",
33
33
  "rxjs": ">=7"
34
34
  },
35
35
  "devDependencies": {
36
- "@tallyui/core": "3.4.0",
36
+ "@tallyui/core": "3.5.0",
37
37
  "@types/react": ">=18",
38
38
  "react": "^19.2.4",
39
39
  "rxdb": "17.5.0",
40
40
  "rxjs": "7.8.2",
41
- "@tallyui/connector-medusa": "3.4.0",
42
- "@tallyui/connector-woocommerce": "3.4.0",
43
- "@tallyui/database": "3.4.0"
41
+ "@tallyui/connector-medusa": "3.5.0",
42
+ "@tallyui/connector-woocommerce": "3.5.0",
43
+ "@tallyui/database": "3.5.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",
package/src/index.ts CHANGED
@@ -55,7 +55,7 @@ export { buildReceiptData } from './receipt';
55
55
  export type { ReceiptData, ReceiptLineItem, ReceiptConfig } from './receipt';
56
56
 
57
57
  // Product
58
- export { searchProducts, withStockOverlay, getProductStock, stockOverlay$, stockOverlayAsOf$ } from './product';
58
+ export { searchProducts, sortProducts, productSortValue, PRODUCT_SORT_FIELDS, type ProductSort, type ProductSortValue, catalogueViewReducer, normalizeCatalogueViewState, resolveGridColumns, DEFAULT_CATALOGUE_VIEW_STATE, type CatalogueView, type CatalogueGridColumns, type CatalogueViewState, type CatalogueViewAction, withStockOverlay, getProductStock, stockOverlay$, stockOverlayAsOf$, productCategories, listCategories, inCategory } from './product';
59
59
 
60
60
  // Sale
61
61
  export { useSale, DISCOUNTS_UNSUPPORTED, SALE_SAVING, saleLogger, addEntryToCart, CartError, catalogueEntries, findEntryByCode, variantPriceLabel } from './sale';
@@ -0,0 +1,60 @@
1
+ import type { ProductSort } from './sort-products';
2
+
3
+ export type CatalogueView = 'grid' | 'table';
4
+ export type CatalogueGridColumns = 'auto' | 2 | 3 | 4 | 5 | 6 | 7 | 8;
5
+ export interface CatalogueViewState {
6
+ view: CatalogueView;
7
+ gridColumns: CatalogueGridColumns;
8
+ sort: ProductSort | null;
9
+ categoryId: string | null;
10
+ }
11
+ export const DEFAULT_CATALOGUE_VIEW_STATE: CatalogueViewState = Object.freeze({
12
+ view: 'grid', gridColumns: 'auto', sort: null, categoryId: null,
13
+ });
14
+
15
+ export type CatalogueViewAction =
16
+ | { type: 'setView'; view: CatalogueView }
17
+ | { type: 'setGridColumns'; gridColumns: CatalogueGridColumns }
18
+ | { type: 'setSort'; sort: ProductSort | null }
19
+ | { type: 'setCategory'; categoryId: string | null }
20
+ | { type: 'replace'; state: CatalogueViewState };
21
+
22
+ export function catalogueViewReducer(state: CatalogueViewState, action: CatalogueViewAction): CatalogueViewState {
23
+ switch (action.type) {
24
+ case 'setView': return state.view === action.view ? state : { ...state, view: action.view };
25
+ case 'setGridColumns': return state.gridColumns === action.gridColumns ? state : { ...state, gridColumns: action.gridColumns };
26
+ case 'setCategory': return state.categoryId === action.categoryId ? state : { ...state, categoryId: action.categoryId };
27
+ case 'setSort': return state.sort?.field === action.sort?.field && state.sort?.dir === action.sort?.dir
28
+ ? state : { ...state, sort: action.sort };
29
+ case 'replace': return action.state;
30
+ }
31
+ }
32
+
33
+ export function normalizeCatalogueViewState(raw: unknown, defaults: CatalogueViewState = DEFAULT_CATALOGUE_VIEW_STATE): CatalogueViewState {
34
+ if (typeof raw !== 'object' || raw === null) return defaults;
35
+ try {
36
+ const { view, gridColumns, sort, categoryId } = raw as Record<string, unknown>;
37
+ const candidate = sort as Partial<ProductSort> | null;
38
+ return {
39
+ categoryId: categoryId === null || (typeof categoryId === 'string' && categoryId.length > 0)
40
+ ? categoryId : defaults.categoryId,
41
+ view: view === 'grid' || view === 'table' ? view : defaults.view,
42
+ gridColumns: gridColumns === 'auto' || (typeof gridColumns === 'number' && Number.isInteger(gridColumns)
43
+ && gridColumns >= 2 && gridColumns <= 8) ? gridColumns as CatalogueGridColumns : defaults.gridColumns,
44
+ sort: sort === null ? null : typeof candidate === 'object' && candidate
45
+ && typeof candidate.field === 'string' && candidate.field.length > 0
46
+ && (candidate.dir === 'asc' || candidate.dir === 'desc')
47
+ ? { field: candidate.field, dir: candidate.dir } : defaults.sort,
48
+ };
49
+ } catch {
50
+ return defaults;
51
+ }
52
+ }
53
+
54
+ // Keep product names readable and touch targets at least 160 px wide where two columns fit.
55
+ const MIN_TILE_WIDTH = 160;
56
+ export function resolveGridColumns(gridColumns: CatalogueGridColumns, containerWidth: number): number {
57
+ // ProductGrid has 4 px padding on each side of the content and each cell: 160 px tile + 8.
58
+ return gridColumns === 'auto'
59
+ ? Math.max(2, Math.min(6, Math.floor((containerWidth - 8) / (MIN_TILE_WIDTH + 8)))) : gridColumns;
60
+ }
@@ -0,0 +1,26 @@
1
+ import type { ProductCategory, ProductTraits } from '@tallyui/core';
2
+
3
+ /** The product's categories: `getCategories`, else `getCategoryNames` with each name as its id. */
4
+ export function productCategories<Doc>(doc: Doc, traits: ProductTraits<Doc>): ProductCategory[] {
5
+ return traits.getCategories
6
+ ? traits.getCategories(doc)
7
+ : traits.getCategoryNames(doc).map((name) => ({ id: name, name }));
8
+ }
9
+
10
+ /** Every category across `docs`, once per id (the first name seen wins), sorted by name (natural, case-insensitive), ties by id. */
11
+ export function listCategories<Doc>(docs: readonly Doc[], traits: ProductTraits<Doc>): ProductCategory[] {
12
+ const categories = new Map<string, ProductCategory>();
13
+ for (const doc of docs) {
14
+ for (const category of productCategories(doc, traits)) {
15
+ if (!categories.has(category.id)) categories.set(category.id, category);
16
+ }
17
+ }
18
+ return [...categories.values()].sort((a, b) =>
19
+ a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' })
20
+ || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
21
+ }
22
+
23
+ /** Whether `doc` is in the category with this id. */
24
+ export function inCategory<Doc>(doc: Doc, categoryId: string, traits: ProductTraits<Doc>): boolean {
25
+ return productCategories(doc, traits).some((category) => category.id === categoryId);
26
+ }
@@ -1,2 +1,5 @@
1
1
  export { searchProducts } from './search-products';
2
+ export { productCategories, listCategories, inCategory } from './categories';
3
+ export { catalogueViewReducer, normalizeCatalogueViewState, resolveGridColumns, DEFAULT_CATALOGUE_VIEW_STATE, type CatalogueView, type CatalogueGridColumns, type CatalogueViewState, type CatalogueViewAction } from './catalogue-view-state';
4
+ export { sortProducts, productSortValue, PRODUCT_SORT_FIELDS, type ProductSort, type ProductSortValue } from './sort-products';
2
5
  export { withStockOverlay, getProductStock, stockOverlay$, stockOverlayAsOf$ } from './stock';
@@ -0,0 +1,51 @@
1
+ import { resolvePrice, resolvePriceRange, type ProductTraits, type TraitContext } from '@tallyui/core';
2
+
3
+ /** A sort on one field: ascending or descending. */
4
+ export interface ProductSort { field: string; dir: 'asc' | 'desc' }
5
+ export type ProductSortValue = string | number | undefined;
6
+ /** The fields productSortValue knows, in the default column order. */
7
+ export const PRODUCT_SORT_FIELDS = ['name', 'sku', 'barcode', 'price', 'stock', 'category'] as const;
8
+
9
+ export function productSortValue<Doc>(
10
+ doc: Doc, field: string, traits: ProductTraits<Doc>, context?: TraitContext,
11
+ ): ProductSortValue {
12
+ let value: ProductSortValue;
13
+ switch (field) {
14
+ case 'name': value = traits.getName(doc); break;
15
+ case 'sku': value = traits.getSku(doc); break;
16
+ case 'barcode': value = traits.getBarcode(doc); break;
17
+ case 'price': {
18
+ const variants = traits.getVariants?.(doc, context);
19
+ value = variants && variants.length >= 2
20
+ ? resolvePriceRange(variants, context?.currency)?.min.amount
21
+ : resolvePrice(traits.getPrices(doc, context), context?.currency)?.current.amount;
22
+ break;
23
+ }
24
+ case 'stock': value = traits.getStock(doc).quantity; break;
25
+ case 'category': value = traits.getCategoryNames(doc).join(', '); break;
26
+ default: return undefined;
27
+ }
28
+ return value === '' ? undefined : value;
29
+ }
30
+
31
+ export function sortProducts<Doc>(
32
+ docs: Doc[], sort: ProductSort | null | undefined,
33
+ valueOf: (doc: Doc, field: string) => ProductSortValue,
34
+ ): Doc[] {
35
+ if (!sort) return docs;
36
+ return docs.map((doc, index) => ({ doc, index, value: valueOf(doc, sort.field) }))
37
+ .sort((a, b) => {
38
+ const aMissing = a.value === undefined || a.value === '';
39
+ const bMissing = b.value === undefined || b.value === '';
40
+ if (aMissing || bMissing) {
41
+ return aMissing === bMissing ? a.index - b.index : aMissing ? 1 : -1;
42
+ }
43
+ const av = a.value!;
44
+ const bv = b.value!;
45
+ const comparison = typeof av === 'number'
46
+ ? (typeof bv === 'number' ? av - bv : -1)
47
+ : (typeof bv === 'number' ? 1 : av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' }));
48
+ return (sort.dir === 'desc' ? -comparison : comparison) || a.index - b.index;
49
+ })
50
+ .map(({ doc }) => doc);
51
+ }