@caseparts-org/casecore 0.0.12 → 0.0.14

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.
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { Pricing } from './ClassPricing';
3
+ export interface BaseCartItem {
4
+ ItemKey: number | string;
5
+ Quantity: number;
6
+ Pricing?: Pricing | undefined;
7
+ [k: string]: any;
8
+ }
9
+ export interface CartContextValue {
10
+ items: BaseCartItem[];
11
+ initialized: boolean;
12
+ isMobile: boolean;
13
+ subtotal: number;
14
+ setItems: (_next: BaseCartItem[] | ((_prev: BaseCartItem[]) => BaseCartItem[])) => void;
15
+ addOrReplace: (_item: BaseCartItem) => void;
16
+ updateQuantity: (_key: number | string, _qty: number) => void;
17
+ removeItem: (_key: number | string) => void;
18
+ clear: () => void;
19
+ }
20
+ export interface CartProviderProps {
21
+ pricingResolver?: (_item: BaseCartItem) => number;
22
+ children: React.ReactNode;
23
+ }
24
+ export declare const CartProvider: React.FC<CartProviderProps>;
25
+ export declare function useCartContext(): CartContextValue;
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react';
3
+ const STORAGE_KEY = 'CPC.cart';
4
+ const CartContext = createContext(undefined);
5
+ export const CartProvider = ({ children, pricingResolver }) => {
6
+ const [items, _setItems] = useState([]);
7
+ const [initialized, setInitialized] = useState(false);
8
+ const [isMobile, setIsMobile] = useState(false);
9
+ // Load once
10
+ useEffect(() => {
11
+ try {
12
+ const raw = typeof window !== 'undefined'
13
+ ? window.localStorage.getItem(STORAGE_KEY)
14
+ : null;
15
+ if (raw) {
16
+ const parsed = JSON.parse(raw);
17
+ if (Array.isArray(parsed?.items)) {
18
+ _setItems(parsed.items);
19
+ }
20
+ }
21
+ }
22
+ catch { /* ignore */ }
23
+ if (typeof navigator !== 'undefined') {
24
+ setIsMobile(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent));
25
+ }
26
+ setInitialized(true);
27
+ }, []);
28
+ const persist = useCallback((next) => {
29
+ try {
30
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ items: next }));
31
+ }
32
+ catch { /* ignore */ }
33
+ }, []);
34
+ const setItems = useCallback((next) => {
35
+ _setItems(prev => {
36
+ const resolvedArray = typeof next === 'function'
37
+ ? next(prev)
38
+ : next;
39
+ const cloned = resolvedArray.map(i => ({ ...i })); // ensure new refs
40
+ persist(cloned);
41
+ return cloned;
42
+ });
43
+ }, [persist]);
44
+ const addOrReplace = useCallback((item) => {
45
+ setItems(prev => {
46
+ const idx = prev.findIndex(p => p.ItemKey === item.ItemKey);
47
+ if (idx === -1)
48
+ return [...prev, { ...item }];
49
+ const next = [...prev];
50
+ next[idx] = { ...item };
51
+ return next;
52
+ });
53
+ }, [setItems]);
54
+ const updateQuantity = useCallback((key, qty) => {
55
+ setItems(prev => {
56
+ if (qty <= 0)
57
+ return prev.filter(i => i.ItemKey !== key);
58
+ return prev.map(i => i.ItemKey === key ? { ...i, Quantity: qty } : i);
59
+ });
60
+ }, [setItems]);
61
+ const removeItem = useCallback((key) => {
62
+ setItems(prev => prev.filter(i => i.ItemKey !== key));
63
+ }, [setItems]);
64
+ const clear = useCallback(() => {
65
+ setItems([]);
66
+ }, [setItems]);
67
+ const subtotal = useMemo(() => {
68
+ if (!items.length)
69
+ return 0;
70
+ return items.reduce((sum, it) => {
71
+ const price = pricingResolver ? pricingResolver(it) : 0;
72
+ return sum + price * (it.Quantity ?? 1);
73
+ }, 0);
74
+ }, [items, pricingResolver]);
75
+ const value = useMemo(() => ({
76
+ items,
77
+ initialized,
78
+ isMobile,
79
+ subtotal,
80
+ setItems,
81
+ addOrReplace,
82
+ updateQuantity,
83
+ removeItem,
84
+ clear
85
+ }), [items, initialized, isMobile, subtotal, setItems, addOrReplace, updateQuantity, removeItem, clear]);
86
+ return (_jsx(CartContext.Provider, { value: value, children: children }));
87
+ };
88
+ export function useCartContext() {
89
+ const ctx = useContext(CartContext);
90
+ if (!ctx)
91
+ throw new Error('useCart must be used inside <CartProvider>');
92
+ return ctx;
93
+ }
package/dist/index.d.ts CHANGED
@@ -8,5 +8,7 @@ export type { MicroFlexFieldSelectors, MicroFlexOptions, MicroFlexReturn } from
8
8
  export { default as useMicroFlex } from './hooks/useMicroFlex';
9
9
  export type { Pricing, CustomerClass } from './cart/ClassPricing';
10
10
  export { getClassPricing } from './cart/ClassPricing';
11
- export type { BaseCartItem, Cart, CartConfig, CartContextValue, CreateCartReturn, PricingResolver, CartProviderProps } from './cart/CreateCart';
12
- export { createCart } from './cart/CreateCart';
11
+ export type { BaseCartItem, CartContextValue, CartProviderProps, } from './cart/CartContext';
12
+ export { CartProvider, useCartContext } from './cart/CartContext';
13
+ export type { WarehouseInventory, Inventory, Availability, CustomPartAvailabilityResult, UnifiedAvailabilityResult, } from './inventory/PartAvailability';
14
+ export { default as calcPartAvailability, calcDetailedPartAvailability, CUSTOM_PART_LEAD_TIMES, isDiscontinued, computeStockAvailability, computeCustomPartAvailability, shouldSuggestContactLink, computeAvailability } from './inventory/PartAvailability';
package/dist/index.js CHANGED
@@ -6,4 +6,5 @@ export { getSessionId } from './utils/SessionUtils';
6
6
  export { buildClaimsFromPayload } from './utils/ClaimsUtils';
7
7
  export { default as useMicroFlex } from './hooks/useMicroFlex';
8
8
  export { getClassPricing } from './cart/ClassPricing';
9
- export { createCart } from './cart/CreateCart';
9
+ export { CartProvider, useCartContext } from './cart/CartContext';
10
+ export { default as calcPartAvailability, calcDetailedPartAvailability, CUSTOM_PART_LEAD_TIMES, isDiscontinued, computeStockAvailability, computeCustomPartAvailability, shouldSuggestContactLink, computeAvailability } from './inventory/PartAvailability';
@@ -0,0 +1,36 @@
1
+ export interface WarehouseInventory {
2
+ Unreserved: number;
3
+ Available: number;
4
+ }
5
+ export interface Inventory {
6
+ MPK: WarehouseInventory;
7
+ SEA: WarehouseInventory;
8
+ STL: WarehouseInventory;
9
+ }
10
+ export interface Availability {
11
+ description: string;
12
+ id: string;
13
+ }
14
+ export default function calcPartAvailability(inventory: Inventory | undefined, discontinued: boolean): Availability | '';
15
+ export declare function calcDetailedPartAvailability(inventory: Inventory | undefined, discontinued: boolean): Availability;
16
+ export declare const CUSTOM_PART_LEAD_TIMES: Record<string, string>;
17
+ export declare function isDiscontinued(partStatus: number): boolean;
18
+ export declare function computeStockAvailability(inventory: Inventory | undefined, partStatus: number, detailed: boolean): Availability | '';
19
+ export interface CustomPartAvailabilityResult {
20
+ availability: Availability | null;
21
+ leadTime: string | null;
22
+ }
23
+ export declare function computeCustomPartAvailability(customType: string, _inventory: Inventory | undefined, _partStatus: number): CustomPartAvailabilityResult;
24
+ export declare function shouldSuggestContactLink(availability: Availability, useFullDescription: boolean): boolean;
25
+ export interface UnifiedAvailabilityResult {
26
+ availability: Availability | '' | null;
27
+ leadTime: string | null;
28
+ source: 'custom' | 'stock';
29
+ }
30
+ export declare function computeAvailability(options: {
31
+ isCustom: boolean;
32
+ customType?: string;
33
+ inventory: Inventory | undefined;
34
+ partStatus: number;
35
+ detailed?: boolean;
36
+ }): UnifiedAvailabilityResult;
@@ -0,0 +1,117 @@
1
+ export default function calcPartAvailability(inventory, discontinued) {
2
+ if (!inventory || !inventory.MPK && !inventory.SEA && !inventory.STL) {
3
+ return '';
4
+ }
5
+ const inStock = inventory.MPK.Unreserved > 5 || inventory.SEA.Unreserved > 5 || inventory.STL.Unreserved > 5;
6
+ const limited = inventory.MPK.Unreserved > 0 || inventory.SEA.Unreserved > 0 || inventory.STL.Unreserved > 0;
7
+ if (inStock) {
8
+ return { description: 'In Stock', id: 'available' };
9
+ }
10
+ else if (discontinued) {
11
+ return { description: "Discontinued", id: "discontinued" };
12
+ }
13
+ else if (limited) {
14
+ return { description: 'Limited availability', id: 'limited' };
15
+ }
16
+ return { description: 'Contact us', id: 'contactus' };
17
+ }
18
+ export function calcDetailedPartAvailability(inventory, discontinued) {
19
+ const unreserved = (whse) => inventory ? inventory[whse].Unreserved : inventory[whse];
20
+ const available = (whse) => inventory ? inventory[whse].Available : inventory[whse];
21
+ const quantity = 1;
22
+ // Deal with discontinued parts
23
+ if (discontinued) {
24
+ const total = unreserved("MPK") + unreserved("STL") + unreserved("SEA");
25
+ const message = total >= quantity
26
+ ? { description: "This part is discontinued, supplies are limited", id: "limited" }
27
+ : total > 0
28
+ ? { description: `This part is discontinued and we only have ${total} in stock`, id: 'available' }
29
+ : { description: "Contact us. This part is discontinued but we might have compatible alternatives.", id: "contactus" };
30
+ return message;
31
+ }
32
+ // Count up the number of warehouses with sufficient unreserved quantity
33
+ const mpk = unreserved("MPK") >= quantity ? 1 : 0;
34
+ const stl = unreserved("STL") >= quantity ? 1 : 0;
35
+ const sea = unreserved("SEA") >= quantity ? 1 : 0;
36
+ const whseCount = mpk + stl + sea;
37
+ if (unreserved("MPK") < 5 && unreserved("SEA") < 5 && unreserved("STL") < 5) {
38
+ let limitedStockDescr = null;
39
+ if (whseCount === 3)
40
+ limitedStockDescr = 'Limited availability at all warehouses';
41
+ else if (whseCount === 1)
42
+ limitedStockDescr = `Limited availability in ${mpk ? "Los Angeles" : stl ? "St. Louis" : "Seattle"}`;
43
+ else if (mpk && stl)
44
+ limitedStockDescr = 'Limited availability in Los Angeles & St. Louis';
45
+ else if (mpk && sea)
46
+ limitedStockDescr = 'Limited availability in Los Angeles & Seattle';
47
+ else if (stl && sea)
48
+ limitedStockDescr = 'Limited availability in St. Louis & Seattle';
49
+ if (limitedStockDescr) {
50
+ return { description: limitedStockDescr, id: 'limited' };
51
+ }
52
+ }
53
+ let inStockDescr = null;
54
+ if (whseCount === 3)
55
+ inStockDescr = 'In Stock at All Warehouses';
56
+ else if (whseCount === 1)
57
+ inStockDescr = `In stock in ${mpk ? "Los Angeles" : stl ? "St. Louis" : "Seattle"}`;
58
+ else if (mpk && stl)
59
+ inStockDescr = 'In Stock in Los Angeles & St. Louis';
60
+ else if (mpk && sea)
61
+ inStockDescr = 'In Stock in Los Angeles & Seattle';
62
+ else if (stl && sea)
63
+ inStockDescr = 'In Stock in St. Louis & Seattle';
64
+ if (inStockDescr) {
65
+ return { description: inStockDescr, id: 'available' };
66
+ }
67
+ if (available("MPK") + available("STL") + available("SEA") >= quantity) {
68
+ return { description: 'Limited availability', id: 'limited' };
69
+ }
70
+ return { description: 'Contact us for availability', id: 'contactus' };
71
+ }
72
+ // Map of custom part lead times
73
+ export const CUSTOM_PART_LEAD_TIMES = {
74
+ Heater: "1 Business Day or Less",
75
+ Wire: "1 Business Day or Less",
76
+ Gasket: "1-2 Business Days",
77
+ };
78
+ // Centralized discontinued check
79
+ export function isDiscontinued(partStatus) {
80
+ return partStatus === 3;
81
+ }
82
+ // Computes availability for a stock (non-custom) part.
83
+ // Wrapper around existing calcPartAvailability / calcDetailedPartAvailability.
84
+ export function computeStockAvailability(inventory, partStatus, detailed) {
85
+ const discontinued = isDiscontinued(partStatus);
86
+ return detailed
87
+ ? calcDetailedPartAvailability(inventory, discontinued)
88
+ : calcPartAvailability(inventory, discontinued);
89
+ }
90
+ // Computes availability info for a custom part (e.g. Heater, Wire, Gasket).
91
+ export function computeCustomPartAvailability(customType, _inventory, _partStatus) {
92
+ const leadTime = CUSTOM_PART_LEAD_TIMES[customType] || null;
93
+ return { availability: null, leadTime };
94
+ }
95
+ // Helper to decide whether UI should render a contact link
96
+ export function shouldSuggestContactLink(availability, useFullDescription) {
97
+ return availability.id === "contactus" && useFullDescription;
98
+ }
99
+ // Wrapper choosing custom vs stock logic.
100
+ // isCustom: true requires customType.
101
+ // detailed applies only to stock (ignored for custom unless you later extend).
102
+ export function computeAvailability(options) {
103
+ const { isCustom, customType, inventory, partStatus, detailed = false } = options;
104
+ if (isCustom) {
105
+ if (!customType) {
106
+ return { availability: null, leadTime: null, source: 'custom' };
107
+ }
108
+ if (customType === 'Heater') {
109
+ const availability = computeStockAvailability(inventory, partStatus, true);
110
+ return { availability, leadTime: null, source: 'stock' };
111
+ }
112
+ const { availability, leadTime } = computeCustomPartAvailability(customType, inventory, partStatus);
113
+ return { availability, leadTime, source: 'custom' };
114
+ }
115
+ const availability = computeStockAvailability(inventory, partStatus, detailed);
116
+ return { availability, leadTime: null, source: 'stock' };
117
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@caseparts-org/casecore",
3
3
  "private": false,
4
- "version": "0.0.12",
4
+ "version": "0.0.14",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -1,31 +0,0 @@
1
- import React from 'react';
2
- import { Pricing } from './ClassPricing';
3
- export type PricingResolver<T extends BaseCartItem> = (_item: T) => number;
4
- export interface BaseCartItem {
5
- ItemKey: number | string;
6
- Quantity: number;
7
- Pricing?: Pricing | undefined;
8
- [k: string]: any;
9
- }
10
- export interface Cart<T extends BaseCartItem = BaseCartItem> {
11
- items: T[];
12
- }
13
- export interface CartConfig<T extends BaseCartItem> {
14
- storageKey?: string;
15
- pricingResolver?: PricingResolver<T>;
16
- }
17
- export interface CartContextValue<T extends BaseCartItem> {
18
- cart: Cart<T>;
19
- updateCart: (_cart: Cart<T>) => void;
20
- cartSubtotal: () => number;
21
- initializedCart: boolean;
22
- isMobile: boolean;
23
- }
24
- export type CartProviderProps<T extends BaseCartItem> = React.PropsWithChildren<{
25
- pricingResolver?: PricingResolver<T>;
26
- }>;
27
- export interface CreateCartReturn<T extends BaseCartItem> {
28
- CartProvider: React.FC<CartProviderProps<T>>;
29
- useCartContext: () => CartContextValue<T>;
30
- }
31
- export declare function createCart<T extends BaseCartItem = BaseCartItem>(config?: CartConfig<T>): CreateCartReturn<T>;
@@ -1,65 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
3
- export function createCart(config) {
4
- const STORAGE_KEY = config?.storageKey ?? 'CPC.cart';
5
- const CartContext = createContext(undefined);
6
- const CartProvider = ({ children, pricingResolver }) => {
7
- const [cart, setCart] = useState({ items: [] });
8
- const [initialized, setInitialized] = useState(false);
9
- const [isMobile, setIsMobile] = useState(false);
10
- const resolver = pricingResolver ?? config?.pricingResolver;
11
- // Initialize cart from local storage
12
- useEffect(() => {
13
- try {
14
- const rawCart = typeof window !== 'undefined' ? window.localStorage.getItem(STORAGE_KEY) : null;
15
- if (rawCart) {
16
- const parsed = JSON.parse(rawCart);
17
- if (parsed?.items && Array.isArray(parsed.items)) {
18
- setCart({ items: parsed.items });
19
- }
20
- }
21
- }
22
- catch {
23
- /* ignore */
24
- }
25
- if (typeof navigator !== 'undefined') {
26
- setIsMobile(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent));
27
- }
28
- setInitialized(true);
29
- }, []);
30
- const save = useCallback((c) => {
31
- try {
32
- window.localStorage.setItem(STORAGE_KEY, JSON.stringify(c));
33
- }
34
- catch { /* suppress */ }
35
- }, []);
36
- const updateCart = useCallback((c) => {
37
- setCart({ items: [...c.items] });
38
- save(c);
39
- }, [save]);
40
- const cartSubtotal = useCallback(() => {
41
- if (!cart.items.length)
42
- return 0;
43
- return cart.items.reduce((sum, item) => {
44
- const price = resolver ? resolver(item) : 0;
45
- const qty = item.Quantity ?? 1;
46
- return sum + price * qty;
47
- }, 0);
48
- }, [cart.items, resolver]);
49
- const value = useMemo(() => ({
50
- cart,
51
- updateCart,
52
- cartSubtotal,
53
- initializedCart: initialized,
54
- isMobile
55
- }), [cart, updateCart, cartSubtotal, initialized, isMobile]);
56
- return _jsx(CartContext.Provider, { value: value, children: children });
57
- };
58
- const useCartContext = () => {
59
- const cartContext = useContext(CartContext);
60
- if (!cartContext)
61
- throw new Error('useCartContext must be used inside CartProvider');
62
- return cartContext;
63
- };
64
- return { CartProvider, useCartContext };
65
- }