@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 +49 -2
- package/dist/index.js +118 -4
- package/package.json +6 -6
- package/src/index.ts +1 -1
- package/src/product/catalogue-view-state.ts +60 -0
- package/src/product/categories.ts +26 -0
- package/src/product/index.ts +3 -0
- package/src/product/sort-products.ts +51 -0
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,
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
42
|
-
"@tallyui/connector-woocommerce": "3.
|
|
43
|
-
"@tallyui/database": "3.
|
|
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
|
|
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
|
+
}
|
package/src/product/index.ts
CHANGED
|
@@ -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
|
+
}
|