@pagamio/frontend-commons-lib 0.8.339 → 0.8.341
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/lib/components/ui/ReservedBreakdownPopover.d.ts +48 -0
- package/lib/components/ui/ReservedBreakdownPopover.js +61 -0
- package/lib/components/ui/StockLabel.d.ts +13 -0
- package/lib/components/ui/StockLabel.js +28 -0
- package/lib/components/ui/index.d.ts +4 -0
- package/lib/components/ui/index.js +4 -0
- package/lib/pagamio-table/data-table/types.d.ts +9 -0
- package/lib/shared/hooks/usePagamioTable.d.ts +1 -1
- package/lib/shared/hooks/usePagamioTable.js +13 -1
- package/lib/styles.css +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface ReservedBreakdownItem {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Short label rendered for this row, e.g. "Cart #1234 (active)" */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Optional secondary line, e.g. "expires in 14m" or "Transfer → Store B" */
|
|
7
|
+
detail?: string;
|
|
8
|
+
quantity: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ReservedBreakdownBucket {
|
|
11
|
+
/** Total quantity for this bucket. */
|
|
12
|
+
quantity: number;
|
|
13
|
+
/** Individual contributing records. May be empty even when quantity > 0
|
|
14
|
+
* if the source only exposes aggregate data. */
|
|
15
|
+
items: ReservedBreakdownItem[];
|
|
16
|
+
/** When true, the bucket's `quantity` is informational and is NOT part
|
|
17
|
+
* of the parent `Reserved` total (e.g. pending stock transfers). */
|
|
18
|
+
informational?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface ReservedBreakdownData {
|
|
21
|
+
total: number;
|
|
22
|
+
orders: ReservedBreakdownBucket;
|
|
23
|
+
transfers: ReservedBreakdownBucket;
|
|
24
|
+
allocations: ReservedBreakdownBucket;
|
|
25
|
+
}
|
|
26
|
+
export interface ReservedBreakdownPopoverProps {
|
|
27
|
+
/** Quantity value to render as the clickable trigger. */
|
|
28
|
+
reservedQuantity: number;
|
|
29
|
+
/** Lazy-load: fetcher invoked when the popover opens for the first time. */
|
|
30
|
+
fetch: () => Promise<ReservedBreakdownData>;
|
|
31
|
+
/** Optional className for the trigger button. */
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Render variant — default styles the trigger as a link-like underlined
|
|
34
|
+
* number, matching the dense layouts in inventory tables. */
|
|
35
|
+
variant?: 'link' | 'plain';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Clickable Reserved-quantity cell that opens a popover breaking down the
|
|
39
|
+
* value by source (Orders, Transfers, Allocations). Data-agnostic — the
|
|
40
|
+
* consumer provides a `fetch` callback that resolves the breakdown for the
|
|
41
|
+
* specific stock-item or station-allocation row.
|
|
42
|
+
*
|
|
43
|
+
* The trigger only renders an interactive control when `reservedQuantity > 0`.
|
|
44
|
+
* At zero, it renders the plain quantity text so the table cell stays
|
|
45
|
+
* visually consistent without offering an interaction with nothing to show.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ReservedBreakdownPopover: React.FC<ReservedBreakdownPopoverProps>;
|
|
48
|
+
export default ReservedBreakdownPopover;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from '../../helpers/utils';
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from './Popover';
|
|
5
|
+
const formatQty = (q) => (Number.isInteger(q) ? String(q) : q.toFixed(2));
|
|
6
|
+
/**
|
|
7
|
+
* Clickable Reserved-quantity cell that opens a popover breaking down the
|
|
8
|
+
* value by source (Orders, Transfers, Allocations). Data-agnostic — the
|
|
9
|
+
* consumer provides a `fetch` callback that resolves the breakdown for the
|
|
10
|
+
* specific stock-item or station-allocation row.
|
|
11
|
+
*
|
|
12
|
+
* The trigger only renders an interactive control when `reservedQuantity > 0`.
|
|
13
|
+
* At zero, it renders the plain quantity text so the table cell stays
|
|
14
|
+
* visually consistent without offering an interaction with nothing to show.
|
|
15
|
+
*/
|
|
16
|
+
export const ReservedBreakdownPopover = ({ reservedQuantity, fetch, className, variant = 'link', }) => {
|
|
17
|
+
const [state, setState] = React.useState({
|
|
18
|
+
loading: false,
|
|
19
|
+
data: undefined,
|
|
20
|
+
error: undefined,
|
|
21
|
+
});
|
|
22
|
+
const hasFetchedRef = React.useRef(false);
|
|
23
|
+
if (reservedQuantity <= 0) {
|
|
24
|
+
return _jsx("span", { className: className, children: formatQty(reservedQuantity) });
|
|
25
|
+
}
|
|
26
|
+
const handleOpenChange = (open) => {
|
|
27
|
+
if (!open || hasFetchedRef.current)
|
|
28
|
+
return;
|
|
29
|
+
hasFetchedRef.current = true;
|
|
30
|
+
setState({ loading: true, data: undefined, error: undefined });
|
|
31
|
+
fetch()
|
|
32
|
+
.then((data) => setState({ loading: false, data, error: undefined }))
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
const message = err instanceof Error ? err.message : 'Failed to load reserved breakdown';
|
|
35
|
+
setState({ loading: false, data: undefined, error: message });
|
|
36
|
+
hasFetchedRef.current = false;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const triggerClasses = variant === 'link'
|
|
40
|
+
? 'underline decoration-dotted underline-offset-4 hover:decoration-solid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded'
|
|
41
|
+
: 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded';
|
|
42
|
+
return (_jsxs(Popover, { onOpenChange: handleOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(triggerClasses, className), "aria-label": `View reserved breakdown (${formatQty(reservedQuantity)})`, children: formatQty(reservedQuantity) }) }), _jsx(PopoverContent, { align: "end", className: "w-80", children: _jsx(BreakdownBody, { state: state, reservedQuantity: reservedQuantity }) })] }));
|
|
43
|
+
};
|
|
44
|
+
const BreakdownBody = ({ state, reservedQuantity }) => {
|
|
45
|
+
if (state.loading) {
|
|
46
|
+
return _jsx("div", { className: "text-sm text-muted-foreground", children: "Loading breakdown\u2026" });
|
|
47
|
+
}
|
|
48
|
+
if (state.error) {
|
|
49
|
+
return _jsx("div", { className: "text-sm text-destructive", children: state.error });
|
|
50
|
+
}
|
|
51
|
+
if (!state.data) {
|
|
52
|
+
return _jsx("div", { className: "text-sm text-muted-foreground", children: "No data available" });
|
|
53
|
+
}
|
|
54
|
+
const { orders, transfers, allocations, total } = state.data;
|
|
55
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsx("h4", { className: "text-sm font-semibold", children: "Reserved breakdown" }), _jsx("span", { className: "text-sm font-mono", children: formatQty(total) })] }), _jsx(BucketSection, { title: "Orders", bucket: orders }), _jsx(BucketSection, { title: "Transfers", bucket: transfers }), _jsx(BucketSection, { title: "Allocations", bucket: allocations }), Math.abs(total - reservedQuantity) > 0.001 ? (_jsxs("p", { className: "text-xs text-muted-foreground italic", children: ["Breakdown total (", formatQty(total), ") differs from the row value (", formatQty(reservedQuantity), "). Data may have changed since the row was last loaded."] })) : null] }));
|
|
56
|
+
};
|
|
57
|
+
const BucketSection = ({ title, bucket }) => {
|
|
58
|
+
const isInformational = bucket.informational === true;
|
|
59
|
+
return (_jsxs("section", { children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("span", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [title, isInformational ? (_jsx("span", { className: "ml-1 normal-case font-normal italic text-muted-foreground/80", children: "(informational)" })) : null] }), _jsx("span", { className: "text-sm font-mono", children: formatQty(bucket.quantity) })] }), bucket.items.length > 0 ? (_jsx("ul", { className: "mt-1 space-y-0.5", children: bucket.items.map((item) => (_jsxs("li", { className: "flex items-baseline justify-between gap-2 text-xs", children: [_jsxs("span", { className: "truncate", children: [_jsx("span", { children: item.label }), item.detail ? _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", item.detail] }) : null] }), _jsx("span", { className: "font-mono", children: formatQty(item.quantity) })] }, item.id))) })) : null, isInformational && bucket.items.length === 0 && bucket.quantity > 0 ? (_jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Pending records exist but no detail is available." })) : null, bucket.quantity === 0 && bucket.items.length === 0 ? (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: "None" })) : null] }));
|
|
60
|
+
};
|
|
61
|
+
export default ReservedBreakdownPopover;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type StockLabelKind = 'onHand' | 'reserved' | 'availableToSell';
|
|
3
|
+
export interface StockLabelProps {
|
|
4
|
+
kind: StockLabelKind;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Inventory column label paired with a help-icon tooltip. Single source of
|
|
9
|
+
* truth for the three stock-quantity labels (On Hand / Reserved / Available
|
|
10
|
+
* to Sell) and their help text. Update copy here, not at the call site.
|
|
11
|
+
*/
|
|
12
|
+
export declare const StockLabel: React.FC<StockLabelProps>;
|
|
13
|
+
export default StockLabel;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiInformationCircle } from 'react-icons/hi';
|
|
3
|
+
import { cn } from '../../helpers/utils';
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './Tooltip';
|
|
5
|
+
const STOCK_LABEL_CONFIG = {
|
|
6
|
+
onHand: {
|
|
7
|
+
label: 'On Hand',
|
|
8
|
+
helpText: 'Total stock physically in this location',
|
|
9
|
+
},
|
|
10
|
+
reserved: {
|
|
11
|
+
label: 'Reserved',
|
|
12
|
+
helpText: 'Stock already committed to orders, transfers, or allocations',
|
|
13
|
+
},
|
|
14
|
+
availableToSell: {
|
|
15
|
+
label: 'Available to Sell',
|
|
16
|
+
helpText: 'Stock currently available for sale',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Inventory column label paired with a help-icon tooltip. Single source of
|
|
21
|
+
* truth for the three stock-quantity labels (On Hand / Reserved / Available
|
|
22
|
+
* to Sell) and their help text. Update copy here, not at the call site.
|
|
23
|
+
*/
|
|
24
|
+
export const StockLabel = ({ kind, className }) => {
|
|
25
|
+
const { label, helpText } = STOCK_LABEL_CONFIG[kind];
|
|
26
|
+
return (_jsx(TooltipProvider, { delayDuration: 150, children: _jsxs("span", { className: cn('inline-flex items-center gap-1', className), children: [_jsx("span", { children: label }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": `${label} help`, className: "text-muted-foreground hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-full", children: _jsx(HiInformationCircle, { className: "h-4 w-4", "aria-hidden": "true" }) }) }), _jsx(TooltipContent, { className: "max-w-xs", children: helpText })] })] }) }));
|
|
27
|
+
};
|
|
28
|
+
export default StockLabel;
|
|
@@ -63,6 +63,10 @@ export type { ImageUploaderProps } from './ImageUploader';
|
|
|
63
63
|
export { default as NotificationModal } from './NotificationModal';
|
|
64
64
|
export type { NotificationModalProps } from './NotificationModal';
|
|
65
65
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
|
|
66
|
+
export { StockLabel, default as StockLabelDefault } from './StockLabel';
|
|
67
|
+
export type { StockLabelKind, StockLabelProps } from './StockLabel';
|
|
68
|
+
export { ReservedBreakdownPopover, default as ReservedBreakdownPopoverDefault } from './ReservedBreakdownPopover';
|
|
69
|
+
export type { ReservedBreakdownData, ReservedBreakdownBucket, ReservedBreakdownItem, ReservedBreakdownPopoverProps, } from './ReservedBreakdownPopover';
|
|
66
70
|
export { default as TimerBadge } from './TimerBadge';
|
|
67
71
|
export type { TimerBadgeProps } from './TimerBadge';
|
|
68
72
|
export { Badge, default as BadgeDefault } from './Badge';
|
|
@@ -62,6 +62,10 @@ export { default as ImageUploader } from './ImageUploader';
|
|
|
62
62
|
export { default as NotificationModal } from './NotificationModal';
|
|
63
63
|
// Tooltip - v2 shadcn-style tooltip with Radix
|
|
64
64
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
|
|
65
|
+
// StockLabel - inventory column label + help-icon tooltip
|
|
66
|
+
export { StockLabel, default as StockLabelDefault } from './StockLabel';
|
|
67
|
+
// ReservedBreakdownPopover - click-to-view breakdown of reserved stock
|
|
68
|
+
export { ReservedBreakdownPopover, default as ReservedBreakdownPopoverDefault } from './ReservedBreakdownPopover';
|
|
65
69
|
// TimerBadge - elapsed time with urgency coloring (green → amber → red)
|
|
66
70
|
export { default as TimerBadge } from './TimerBadge';
|
|
67
71
|
// Badge - styled pill/tag component
|
|
@@ -177,6 +177,15 @@ export interface UsePagamioTableConfig<T extends BaseEntity> {
|
|
|
177
177
|
enabled?: boolean;
|
|
178
178
|
staleTime?: number;
|
|
179
179
|
gcTime?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Auto-refetch interval (ms) for tables backed by long-running jobs
|
|
182
|
+
* (exports, imports, async report generation). Pass a fixed number to
|
|
183
|
+
* always poll, `false` to disable, or a predicate `(rows) => number | false`
|
|
184
|
+
* that returns the interval only while any row is still in flight — the
|
|
185
|
+
* predicate runs on every fetch settle so polling stops automatically
|
|
186
|
+
* once everything is terminal.
|
|
187
|
+
*/
|
|
188
|
+
refetchInterval?: number | false | ((rows: T[]) => number | false);
|
|
180
189
|
/**
|
|
181
190
|
* Keep the previous page's rows visible while the next page fetches
|
|
182
191
|
* (TanStack `placeholderData: keepPreviousData`). Defaults to true so
|
|
@@ -17,4 +17,4 @@ export declare const pagamioMapping: ResponseMapping;
|
|
|
17
17
|
* The hook tracks `page` zero-indexed internally; we bump on the way out.
|
|
18
18
|
*/
|
|
19
19
|
export declare const pagamioQueryConfig: QueryParamConfig;
|
|
20
|
-
export declare const usePagamioTable: <T extends BaseEntity>({ queryKey, queryFn, enabled: queryEnabled, staleTime, gcTime, keepPreviousData: keepPrev, data: clientData, columns, defaultFilters, responseMapping, queryParamConfig, pagination: paginationConfig, filtering: filteringConfig, sorting: sortingConfig, toolbar, searchPlaceholder, onRowClick, rowClassName, expandable, renderDetailPanel, }: UsePagamioTableConfig<T>) => UsePagamioTableReturn<T>;
|
|
20
|
+
export declare const usePagamioTable: <T extends BaseEntity>({ queryKey, queryFn, enabled: queryEnabled, staleTime, gcTime, refetchInterval, keepPreviousData: keepPrev, data: clientData, columns, defaultFilters, responseMapping, queryParamConfig, pagination: paginationConfig, filtering: filteringConfig, sorting: sortingConfig, toolbar, searchPlaceholder, onRowClick, rowClassName, expandable, renderDetailPanel, }: UsePagamioTableConfig<T>) => UsePagamioTableReturn<T>;
|
|
@@ -158,7 +158,7 @@ function clientSort(items, sortConfig) {
|
|
|
158
158
|
return sorted;
|
|
159
159
|
}
|
|
160
160
|
// ─── Hook ────────────────────────────────────────────────────────────
|
|
161
|
-
export const usePagamioTable = ({ queryKey, queryFn, enabled: queryEnabled = true, staleTime, gcTime, keepPreviousData: keepPrev = true, data: clientData, columns, defaultFilters = {}, responseMapping = springBootMapping, queryParamConfig = defaultQueryConfig, pagination: paginationConfig = { enabled: true, mode: 'client' }, filtering: filteringConfig = { enabled: true, mode: 'client', searchMode: 'server' }, sorting: sortingConfig = { enabled: true, mode: 'client' }, toolbar, searchPlaceholder, onRowClick, rowClassName, expandable, renderDetailPanel, }) => {
|
|
161
|
+
export const usePagamioTable = ({ queryKey, queryFn, enabled: queryEnabled = true, staleTime, gcTime, refetchInterval, keepPreviousData: keepPrev = true, data: clientData, columns, defaultFilters = {}, responseMapping = springBootMapping, queryParamConfig = defaultQueryConfig, pagination: paginationConfig = { enabled: true, mode: 'client' }, filtering: filteringConfig = { enabled: true, mode: 'client', searchMode: 'server' }, sorting: sortingConfig = { enabled: true, mode: 'client' }, toolbar, searchPlaceholder, onRowClick, rowClassName, expandable, renderDetailPanel, }) => {
|
|
162
162
|
// ── Local state ──────────────────────────────────────────────────
|
|
163
163
|
const [currentPage, setCurrentPage] = useState(paginationConfig.pageIndex ?? 0);
|
|
164
164
|
const [pageSize, setPageSize] = useState(paginationConfig.pageSize ?? 10);
|
|
@@ -190,6 +190,18 @@ export const usePagamioTable = ({ queryKey, queryFn, enabled: queryEnabled = tru
|
|
|
190
190
|
staleTime,
|
|
191
191
|
gcTime,
|
|
192
192
|
placeholderData: keepPrev ? keepPreviousData : undefined,
|
|
193
|
+
// Predicate form re-extracts rows from the raw response so callers can
|
|
194
|
+
// gate polling on row state (e.g. "any pending export") without needing
|
|
195
|
+
// to know the responseMapping shape.
|
|
196
|
+
refetchInterval: typeof refetchInterval === 'function'
|
|
197
|
+
? (query) => {
|
|
198
|
+
const raw = query.state.data;
|
|
199
|
+
if (!raw)
|
|
200
|
+
return false;
|
|
201
|
+
const { data: rows } = processResponse(raw, responseMapping);
|
|
202
|
+
return refetchInterval(rows);
|
|
203
|
+
}
|
|
204
|
+
: refetchInterval,
|
|
193
205
|
});
|
|
194
206
|
// ── Process server response ──────────────────────────────────────
|
|
195
207
|
const { serverData, serverTotal } = useMemo(() => {
|
package/lib/styles.css
CHANGED
|
@@ -3117,6 +3117,9 @@ video {
|
|
|
3117
3117
|
.capitalize {
|
|
3118
3118
|
text-transform: capitalize;
|
|
3119
3119
|
}
|
|
3120
|
+
.normal-case {
|
|
3121
|
+
text-transform: none;
|
|
3122
|
+
}
|
|
3120
3123
|
.italic {
|
|
3121
3124
|
font-style: italic;
|
|
3122
3125
|
}
|
|
@@ -3487,6 +3490,9 @@ video {
|
|
|
3487
3490
|
.line-through {
|
|
3488
3491
|
text-decoration-line: line-through;
|
|
3489
3492
|
}
|
|
3493
|
+
.decoration-dotted {
|
|
3494
|
+
text-decoration-style: dotted;
|
|
3495
|
+
}
|
|
3490
3496
|
.underline-offset-2 {
|
|
3491
3497
|
text-underline-offset: 2px;
|
|
3492
3498
|
}
|
|
@@ -4475,6 +4481,9 @@ video {
|
|
|
4475
4481
|
.hover\:underline:hover {
|
|
4476
4482
|
text-decoration-line: underline;
|
|
4477
4483
|
}
|
|
4484
|
+
.hover\:decoration-solid:hover {
|
|
4485
|
+
text-decoration-style: solid;
|
|
4486
|
+
}
|
|
4478
4487
|
.hover\:opacity-100:hover {
|
|
4479
4488
|
opacity: 1;
|
|
4480
4489
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagamio/frontend-commons-lib",
|
|
3
3
|
"description": "Pagamio library for Frontend reusable components like the form engine and table container",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.341",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|