@tallyui/pos 3.3.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 +55 -3
- package/dist/index.js +142 -10
- package/package.json +6 -6
- package/src/index.ts +1 -1
- package/src/order/order-builder.ts +12 -2
- 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/src/register/session-store.ts +3 -1
- package/src/sale/use-sale.ts +3 -0
- package/src/tax/tax-provider.tsx +4 -2
- package/src/tax/types.ts +2 -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';
|
|
@@ -125,6 +125,8 @@ type TaxRateMap = {
|
|
|
125
125
|
[taxClass: string]: number;
|
|
126
126
|
};
|
|
127
127
|
interface TaxContext {
|
|
128
|
+
shippingTaxClass?: string;
|
|
129
|
+
taxClassSlugs?: readonly string[];
|
|
128
130
|
getTaxRates?(taxClass?: string, options?: {
|
|
129
131
|
shipping?: boolean;
|
|
130
132
|
}): readonly WooRate[];
|
|
@@ -148,7 +150,7 @@ interface TaxProviderProps extends Pick<StoreSettings, 'taxRoundAtSubtotal' | 's
|
|
|
148
150
|
rateCodes?: Record<string, string>;
|
|
149
151
|
children: ReactNode;
|
|
150
152
|
}
|
|
151
|
-
declare function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, children }: TaxProviderProps): react_jsx_runtime.JSX.Element;
|
|
153
|
+
declare function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs, children }: TaxProviderProps): react_jsx_runtime.JSX.Element;
|
|
152
154
|
declare function useTax(): TaxContext;
|
|
153
155
|
|
|
154
156
|
type StoreSettingsResolution = {
|
|
@@ -616,6 +618,53 @@ declare function buildReceiptData(order: SentOrder, config: ReceiptConfig): Rece
|
|
|
616
618
|
*/
|
|
617
619
|
declare function searchProducts<Doc>(docs: Doc[], term: string, traits: ProductTraits<Doc>): Doc[];
|
|
618
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
|
+
|
|
619
668
|
/** The `stock_levels` collection as a live map of key to stock value. */
|
|
620
669
|
declare function stockOverlay$(collection: RxCollection): Observable<Map<string, unknown>>;
|
|
621
670
|
/**
|
|
@@ -1957,6 +2006,9 @@ declare function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
1957
2006
|
stage: SaleStage;
|
|
1958
2007
|
error: string | null;
|
|
1959
2008
|
idle: boolean;
|
|
2009
|
+
/** The store's capabilities as passed in: a component offers fees, shipping and custom lines
|
|
2010
|
+
* only when `orderCreate >= 5`, and their tax status and class only as `lineTax` allows. */
|
|
2011
|
+
capabilities: ServerCapabilities | undefined;
|
|
1960
2012
|
/** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
|
|
1961
2013
|
saving: boolean;
|
|
1962
2014
|
/** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
|
|
@@ -2444,4 +2496,4 @@ declare function planLegs(plan: TenderPlan, rowsSinceFrom: {
|
|
|
2444
2496
|
};
|
|
2445
2497
|
};
|
|
2446
2498
|
|
|
2447
|
-
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
|
@@ -327,7 +327,7 @@ import { woocommerceTax as woocommerceTax2 } from "@tallyui/core";
|
|
|
327
327
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
328
328
|
var taxLogger = createLogger("tax");
|
|
329
329
|
var TaxCtx = createContext2(null);
|
|
330
|
-
function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, children }) {
|
|
330
|
+
function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs, children }) {
|
|
331
331
|
const value = useMemo2(
|
|
332
332
|
() => {
|
|
333
333
|
if (Object.values(ratesPpm).some((rate) => !Number.isSafeInteger(rate) || rate < 0)) {
|
|
@@ -350,10 +350,12 @@ function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates
|
|
|
350
350
|
return rateCodes && Object.hasOwn(rateCodes, key) ? rateCodes[key] : void 0;
|
|
351
351
|
},
|
|
352
352
|
pricesIncludeTax,
|
|
353
|
-
...rounding ? { rounding } : {}
|
|
353
|
+
...rounding ? { rounding } : {},
|
|
354
|
+
...shippingTaxClass !== void 0 ? { shippingTaxClass } : {},
|
|
355
|
+
...taxClassSlugs !== void 0 ? { taxClassSlugs } : {}
|
|
354
356
|
};
|
|
355
357
|
},
|
|
356
|
-
[ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates]
|
|
358
|
+
[ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs]
|
|
357
359
|
);
|
|
358
360
|
return /* @__PURE__ */ jsx2(TaxCtx.Provider, { value, children });
|
|
359
361
|
}
|
|
@@ -554,7 +556,7 @@ function createRepository(collection) {
|
|
|
554
556
|
|
|
555
557
|
// src/order/order-builder.ts
|
|
556
558
|
import { BehaviorSubject } from "rxjs";
|
|
557
|
-
import { resolvePrice } from "@tallyui/core";
|
|
559
|
+
import { resolvePrice, woocommerceTax as woocommerceTax3 } from "@tallyui/core";
|
|
558
560
|
|
|
559
561
|
// src/order/allocate-order-discount.ts
|
|
560
562
|
function allocateOrderDiscount(lineAmountsMinor, totalMinor) {
|
|
@@ -682,6 +684,7 @@ function createOrderBuilder(options) {
|
|
|
682
684
|
});
|
|
683
685
|
const shares = allocateOrderDiscount(lineAmounts, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
|
|
684
686
|
const lines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
|
|
687
|
+
if (woo) shipping = shipping.map((charge) => ({ ...charge, ...recalculateCharge(charge.id, charge, true) }));
|
|
685
688
|
const wooTotals = woo ? woocommerceTotals(lines, fees, shipping, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax) : void 0;
|
|
686
689
|
const { subtotalMinor } = wooTotals ?? sumLines(lines, taxContext.rounding);
|
|
687
690
|
const charges = [...fees, ...shipping];
|
|
@@ -799,7 +802,18 @@ function createOrderBuilder(options) {
|
|
|
799
802
|
if (!input.name.trim()) throw new RangeError("Name must not be empty");
|
|
800
803
|
const taxStatus = input.taxStatus ?? "taxable";
|
|
801
804
|
if (woo) {
|
|
802
|
-
const
|
|
805
|
+
const taxClass = shipping2 ? taxContext.shippingTaxClass === void 0 || taxContext.shippingTaxClass === "inherit" ? woocommerceTax3.resolveInheritedShippingTaxClass(lineItems.map((line3) => ({
|
|
806
|
+
tax_class: line3.taxClass,
|
|
807
|
+
taxable: line3.taxStatus !== "none"
|
|
808
|
+
})), taxContext.taxClassSlugs ?? []) : taxContext.shippingTaxClass : input.taxClass;
|
|
809
|
+
const untaxed = shipping2 ? taxClass === woocommerceTax3.NO_SHIPPING_TAX : taxStatus === "none";
|
|
810
|
+
const line2 = wooLine(
|
|
811
|
+
input.amountMinor,
|
|
812
|
+
taxClass === woocommerceTax3.NO_SHIPPING_TAX ? void 0 : taxClass,
|
|
813
|
+
taxContext.pricesIncludeTax,
|
|
814
|
+
untaxed,
|
|
815
|
+
shipping2
|
|
816
|
+
);
|
|
803
817
|
return {
|
|
804
818
|
id: id2,
|
|
805
819
|
name: input.name,
|
|
@@ -1440,6 +1454,110 @@ function searchProducts(docs, term, traits) {
|
|
|
1440
1454
|
});
|
|
1441
1455
|
}
|
|
1442
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
|
+
|
|
1443
1561
|
// src/product/stock.ts
|
|
1444
1562
|
import { map as map3 } from "rxjs";
|
|
1445
1563
|
import { STOCK_LEVELS_LAST_PASS } from "@tallyui/core";
|
|
@@ -2882,7 +3000,8 @@ async function writeClosure({
|
|
|
2882
3000
|
const rounding = recorded.granularity === "custom" ? DEFAULT_TAX_ROUNDING : recorded;
|
|
2883
3001
|
roundings.add(rounding.granularity === "woocommerce" ? `woocommerce ${rounding.roundAtSubtotal}` : `${rounding.granularity} ${rounding.mode}`);
|
|
2884
3002
|
const lines = order.lines.map((line) => ({ ...line, taxInclusive: line.taxInclusive ?? order.pricesIncludeTax }));
|
|
2885
|
-
|
|
3003
|
+
const taxedLines = [...lines, ...[...order.fees ?? [], ...order.shipping ?? []].map((charge) => ({ ...charge, taxInclusive: order.pricesIncludeTax }))];
|
|
3004
|
+
for (const { ratePpm, netMinor, amountMinor } of taxLinesByRate(taxedLines, order.taxMinor, void 0, order.taxRounding)) {
|
|
2886
3005
|
const existing2 = taxRates.get(String(ratePpm));
|
|
2887
3006
|
taxRates.set(String(ratePpm), {
|
|
2888
3007
|
ratePpm,
|
|
@@ -3890,7 +4009,7 @@ function useRegisterSession(options) {
|
|
|
3890
4009
|
}
|
|
3891
4010
|
|
|
3892
4011
|
// src/sale/cart.ts
|
|
3893
|
-
import { resolvePrice as
|
|
4012
|
+
import { resolvePrice as resolvePrice3 } from "@tallyui/core";
|
|
3894
4013
|
var CartError = class extends Error {
|
|
3895
4014
|
};
|
|
3896
4015
|
function addEntryToCart(builder, entry, traits, currency) {
|
|
@@ -3901,7 +4020,7 @@ function addEntryToCart(builder, entry, traits, currency) {
|
|
|
3901
4020
|
return existing.id;
|
|
3902
4021
|
}
|
|
3903
4022
|
const name = traits.getName(product) + (traits.getVariantCount(product) > 1 ? ` \xB7 ${variant.title}` : "");
|
|
3904
|
-
const unitPrice =
|
|
4023
|
+
const unitPrice = resolvePrice3(variant.prices, currency)?.current;
|
|
3905
4024
|
if (!unitPrice) throw new CartError(`No ${currency} price for ${name}`);
|
|
3906
4025
|
return builder.addLine({
|
|
3907
4026
|
productId: traits.getId(product),
|
|
@@ -4079,6 +4198,9 @@ function useSale(settings, opts) {
|
|
|
4079
4198
|
stage,
|
|
4080
4199
|
error,
|
|
4081
4200
|
idle: idle3,
|
|
4201
|
+
/** The store's capabilities as passed in: a component offers fees, shipping and custom lines
|
|
4202
|
+
* only when `orderCreate >= 5`, and their tax status and class only as `lineTax` allows. */
|
|
4203
|
+
capabilities: opts.capabilities,
|
|
4082
4204
|
/** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
|
|
4083
4205
|
saving,
|
|
4084
4206
|
/** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
|
|
@@ -4406,7 +4528,7 @@ function useSale(settings, opts) {
|
|
|
4406
4528
|
}
|
|
4407
4529
|
|
|
4408
4530
|
// src/sale/catalogue.ts
|
|
4409
|
-
import { findVariantByCode, formatMoney as formatMoney2, resolvePrice as
|
|
4531
|
+
import { findVariantByCode, formatMoney as formatMoney2, resolvePrice as resolvePrice4 } from "@tallyui/core";
|
|
4410
4532
|
function catalogueEntries(products, traits, context) {
|
|
4411
4533
|
return products.flatMap((product) => {
|
|
4412
4534
|
const variants = traits.getVariants ? traits.getVariants(product, context) : traits.hasVariants(product) ? [] : [{
|
|
@@ -4424,7 +4546,7 @@ function findEntryByCode(entries, code) {
|
|
|
4424
4546
|
return variant ? entries.find((entry) => entry.variant === variant) : void 0;
|
|
4425
4547
|
}
|
|
4426
4548
|
function variantPriceLabel(variant, currency, locale) {
|
|
4427
|
-
const price =
|
|
4549
|
+
const price = resolvePrice4(variant.prices, currency);
|
|
4428
4550
|
return price ? formatMoney2(price.current, locale) : void 0;
|
|
4429
4551
|
}
|
|
4430
4552
|
|
|
@@ -5588,10 +5710,12 @@ function planLegs(plan, rowsSinceFrom, balanceMinor) {
|
|
|
5588
5710
|
export {
|
|
5589
5711
|
CartError,
|
|
5590
5712
|
CurrencyProvider,
|
|
5713
|
+
DEFAULT_CATALOGUE_VIEW_STATE,
|
|
5591
5714
|
DISCOUNTS_UNSUPPORTED,
|
|
5592
5715
|
MAX_TENDER_MINOR,
|
|
5593
5716
|
MICROS_PER_MINOR,
|
|
5594
5717
|
OrderContentMismatchError,
|
|
5718
|
+
PRODUCT_SORT_FIELDS,
|
|
5595
5719
|
PosOrderOpenClosedError,
|
|
5596
5720
|
RegisterApprovalRequiredError,
|
|
5597
5721
|
RegisterCloseIncompleteError,
|
|
@@ -5621,6 +5745,7 @@ export {
|
|
|
5621
5745
|
callbackSink,
|
|
5622
5746
|
cashMovementSchema,
|
|
5623
5747
|
catalogueEntries,
|
|
5748
|
+
catalogueViewReducer,
|
|
5624
5749
|
changeMinor,
|
|
5625
5750
|
clampClosureScope,
|
|
5626
5751
|
closeNeedsApproval,
|
|
@@ -5652,10 +5777,12 @@ export {
|
|
|
5652
5777
|
getBoundRegisterId,
|
|
5653
5778
|
getDeviceId,
|
|
5654
5779
|
getProductStock,
|
|
5780
|
+
inCategory,
|
|
5655
5781
|
initTenderState,
|
|
5656
5782
|
initialTenderState,
|
|
5657
5783
|
isServerDecimal,
|
|
5658
5784
|
labelKeys,
|
|
5785
|
+
listCategories,
|
|
5659
5786
|
minorToDecimal,
|
|
5660
5787
|
mintClosureNumber,
|
|
5661
5788
|
mintUuid,
|
|
@@ -5664,6 +5791,7 @@ export {
|
|
|
5664
5791
|
needsAttention,
|
|
5665
5792
|
nextSaleCounter,
|
|
5666
5793
|
normalizeAmount,
|
|
5794
|
+
normalizeCatalogueViewState,
|
|
5667
5795
|
observeRegister$,
|
|
5668
5796
|
openSession,
|
|
5669
5797
|
openSessionSelector,
|
|
@@ -5676,6 +5804,8 @@ export {
|
|
|
5676
5804
|
posOrderCollection,
|
|
5677
5805
|
posOrderSchema,
|
|
5678
5806
|
posOrdersLogger,
|
|
5807
|
+
productCategories,
|
|
5808
|
+
productSortValue,
|
|
5679
5809
|
quickTenderedAmounts,
|
|
5680
5810
|
ratePpmFromPercent,
|
|
5681
5811
|
readBoundRegister,
|
|
@@ -5691,6 +5821,7 @@ export {
|
|
|
5691
5821
|
registerSessionCollection,
|
|
5692
5822
|
registerSessionSchema,
|
|
5693
5823
|
requireOpenSession,
|
|
5824
|
+
resolveGridColumns,
|
|
5694
5825
|
resolveStoreSettings,
|
|
5695
5826
|
restoreOrderDraft,
|
|
5696
5827
|
roundMicrosToMinor,
|
|
@@ -5700,6 +5831,7 @@ export {
|
|
|
5700
5831
|
selectClosureRows,
|
|
5701
5832
|
sessionOpenCommand,
|
|
5702
5833
|
sessionTransitionCommand,
|
|
5834
|
+
sortProducts,
|
|
5703
5835
|
stampSession,
|
|
5704
5836
|
startCounting,
|
|
5705
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';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
|
-
import { resolvePrice, type ProductTraits, type TaxRounding } from '@tallyui/core';
|
|
2
|
+
import { resolvePrice, woocommerceTax, type ProductTraits, type TaxRounding } from '@tallyui/core';
|
|
3
3
|
import type { TaxContext } from '../tax/types';
|
|
4
4
|
import { taxMicros, roundMicrosToMinor, roundedTaxByRate } from '../tax/exact';
|
|
5
5
|
import { taxLogger } from '../tax/tax-provider';
|
|
@@ -188,6 +188,7 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
|
|
|
188
188
|
const shares = allocateOrderDiscount(lineAmounts, recalcedOrderDiscounts.reduce((sum, d) => sum + d.amountMinor, 0));
|
|
189
189
|
const lines = lineItems.map((li, index) => recalculateLine(li, shares[index]));
|
|
190
190
|
|
|
191
|
+
if (woo) shipping = shipping.map((charge) => ({ ...charge, ...recalculateCharge(charge.id, charge, true) }));
|
|
191
192
|
const wooTotals = woo ? woocommerceTotals(lines, fees, shipping, currency, woo.roundAtSubtotal, taxContext.pricesIncludeTax) : undefined;
|
|
192
193
|
const { subtotalMinor } = wooTotals ?? sumLines(lines, taxContext.rounding);
|
|
193
194
|
const charges = [...fees, ...shipping];
|
|
@@ -310,7 +311,16 @@ export function createOrderBuilder(options: OrderBuilderOptions): OrderBuilder {
|
|
|
310
311
|
if (!input.name.trim()) throw new RangeError('Name must not be empty');
|
|
311
312
|
const taxStatus = input.taxStatus ?? 'taxable';
|
|
312
313
|
if (woo) {
|
|
313
|
-
const
|
|
314
|
+
const taxClass = shipping
|
|
315
|
+
? taxContext.shippingTaxClass === undefined || taxContext.shippingTaxClass === 'inherit'
|
|
316
|
+
? woocommerceTax.resolveInheritedShippingTaxClass(lineItems.map((line) => ({
|
|
317
|
+
tax_class: line.taxClass, taxable: line.taxStatus !== 'none',
|
|
318
|
+
})), taxContext.taxClassSlugs ?? [])
|
|
319
|
+
: taxContext.shippingTaxClass
|
|
320
|
+
: input.taxClass;
|
|
321
|
+
const untaxed = shipping ? taxClass === woocommerceTax.NO_SHIPPING_TAX : taxStatus === 'none';
|
|
322
|
+
const line = wooLine(input.amountMinor, taxClass === woocommerceTax.NO_SHIPPING_TAX ? undefined : taxClass,
|
|
323
|
+
taxContext.pricesIncludeTax, untaxed, shipping);
|
|
314
324
|
return { id, name: input.name, amountMinor: input.amountMinor, taxClass: input.taxClass, taxStatus,
|
|
315
325
|
...line };
|
|
316
326
|
}
|
|
@@ -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
|
+
}
|
|
@@ -484,7 +484,9 @@ export async function writeClosure({
|
|
|
484
484
|
const rounding = recorded.granularity === 'custom' ? DEFAULT_TAX_ROUNDING : recorded;
|
|
485
485
|
roundings.add(rounding.granularity === 'woocommerce' ? `woocommerce ${rounding.roundAtSubtotal}` : `${rounding.granularity} ${rounding.mode}`);
|
|
486
486
|
const lines = order.lines.map((line) => ({ ...line, taxInclusive: line.taxInclusive ?? order.pricesIncludeTax }));
|
|
487
|
-
|
|
487
|
+
const taxedLines = [...lines, ...[...(order.fees ?? []), ...(order.shipping ?? [])]
|
|
488
|
+
.map((charge) => ({ ...charge, taxInclusive: order.pricesIncludeTax }))];
|
|
489
|
+
for (const { ratePpm, netMinor, amountMinor } of taxLinesByRate(taxedLines, order.taxMinor, undefined, order.taxRounding)) {
|
|
488
490
|
const existing = taxRates.get(String(ratePpm));
|
|
489
491
|
taxRates.set(String(ratePpm), {
|
|
490
492
|
ratePpm,
|
package/src/sale/use-sale.ts
CHANGED
|
@@ -249,6 +249,9 @@ export function useSale(settings: Pick<StoreSettings, 'currency'>, opts: {
|
|
|
249
249
|
|
|
250
250
|
const result = {
|
|
251
251
|
order, stage, error, idle,
|
|
252
|
+
/** The store's capabilities as passed in: a component offers fees, shipping and custom lines
|
|
253
|
+
* only when `orderCreate >= 5`, and their tax status and class only as `lineTax` allows. */
|
|
254
|
+
capabilities: opts.capabilities,
|
|
252
255
|
/** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
|
|
253
256
|
saving,
|
|
254
257
|
/** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
|
package/src/tax/tax-provider.tsx
CHANGED
|
@@ -19,7 +19,7 @@ export interface TaxProviderProps extends Pick<StoreSettings, 'taxRoundAtSubtota
|
|
|
19
19
|
children: ReactNode;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, children }: TaxProviderProps) {
|
|
22
|
+
export function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs, children }: TaxProviderProps) {
|
|
23
23
|
const value = useMemo<TaxContext>(
|
|
24
24
|
() => {
|
|
25
25
|
if (Object.values(ratesPpm).some((rate) => !Number.isSafeInteger(rate) || rate < 0)) {
|
|
@@ -45,9 +45,11 @@ export function TaxProvider({ ratesPpm, pricesIncludeTax, rounding, rateCodes, t
|
|
|
45
45
|
},
|
|
46
46
|
pricesIncludeTax,
|
|
47
47
|
...(rounding ? { rounding } : {}),
|
|
48
|
+
...(shippingTaxClass !== undefined ? { shippingTaxClass } : {}),
|
|
49
|
+
...(taxClassSlugs !== undefined ? { taxClassSlugs } : {}),
|
|
48
50
|
};
|
|
49
51
|
},
|
|
50
|
-
[ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates],
|
|
52
|
+
[ratesPpm, pricesIncludeTax, rounding, rateCodes, taxRates, shippingTaxClass, taxClassSlugs],
|
|
51
53
|
);
|
|
52
54
|
|
|
53
55
|
return <TaxCtx.Provider value={value}>{children}</TaxCtx.Provider>;
|
package/src/tax/types.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type TaxRateMap = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export interface TaxContext {
|
|
12
|
+
shippingTaxClass?: string;
|
|
13
|
+
taxClassSlugs?: readonly string[];
|
|
12
14
|
getTaxRates?(taxClass?: string, options?: { shipping?: boolean }): readonly WooRate[];
|
|
13
15
|
/** Tax rate for a tax class as integer parts per million (19% = 190000); the default class when omitted or unknown. */
|
|
14
16
|
getTaxRatePpm(taxClass?: string): number;
|