@wealthx/shadcn 1.5.3 → 1.5.5

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.
@@ -8,46 +8,39 @@ import {
8
8
  } from "@react-awesome-query-builder/ui";
9
9
  import type { AlertQueryField, AlertQueryOperator } from "./types";
10
10
 
11
+ const NUMERIC_OPERATORS_DEFAULT: AlertQueryOperator[] = [
12
+ "equal",
13
+ "less",
14
+ "less_or_equal",
15
+ "greater",
16
+ "greater_or_equal",
17
+ ];
18
+
11
19
  export const ALERT_QUERY_FIELDS: AlertQueryField[] = [
12
20
  {
13
21
  key: "userMetric.max_loan_amount",
14
22
  label: "Borrowing Capacity",
15
23
  type: "number",
16
24
  unit: "dollar",
17
- operators: [
18
- "equal",
19
- "less",
20
- "less_or_equal",
21
- "greater",
22
- "greater_or_equal",
23
- "between",
24
- ],
25
+ operators: NUMERIC_OPERATORS_DEFAULT,
26
+ min: 0,
25
27
  },
26
28
  {
27
29
  key: "userMetric.debt_outstanding",
28
30
  label: "Outstanding Debt",
29
31
  type: "number",
30
32
  unit: "dollar",
31
- operators: [
32
- "equal",
33
- "less",
34
- "less_or_equal",
35
- "greater",
36
- "greater_or_equal",
37
- ],
33
+ operators: NUMERIC_OPERATORS_DEFAULT,
34
+ min: 0,
38
35
  },
39
36
  {
40
37
  key: "userMetric.lvr",
41
38
  label: "Current LVR",
42
39
  type: "number",
43
40
  unit: "percent",
44
- operators: [
45
- "equal",
46
- "less",
47
- "less_or_equal",
48
- "greater",
49
- "greater_or_equal",
50
- ],
41
+ operators: NUMERIC_OPERATORS_DEFAULT,
42
+ min: 0,
43
+ max: 100,
51
44
  },
52
45
  {
53
46
  key: "userMetric.has_met_buying_goal",
@@ -59,79 +52,52 @@ export const ALERT_QUERY_FIELDS: AlertQueryField[] = [
59
52
  label: "Excess Monthly Surplus",
60
53
  type: "number",
61
54
  unit: "dollar",
62
- operators: [
63
- "equal",
64
- "less",
65
- "less_or_equal",
66
- "greater",
67
- "greater_or_equal",
68
- ],
55
+ operators: NUMERIC_OPERATORS_DEFAULT,
56
+ min: 0,
69
57
  },
70
58
  {
71
59
  key: "userMetric.equity",
72
60
  label: "Equity Amount",
73
61
  type: "number",
74
62
  unit: "dollar",
75
- operators: [
76
- "equal",
77
- "less",
78
- "less_or_equal",
79
- "greater",
80
- "greater_or_equal",
81
- ],
63
+ operators: NUMERIC_OPERATORS_DEFAULT,
64
+ min: 0,
82
65
  },
83
66
  {
84
67
  key: "userMetric.max_debt_interest_rate",
85
68
  label: "Max Debt Interest Rate",
86
69
  type: "number",
87
70
  unit: "percent",
88
- operators: [
89
- "equal",
90
- "less",
91
- "less_or_equal",
92
- "greater",
93
- "greater_or_equal",
94
- ],
71
+ operators: NUMERIC_OPERATORS_DEFAULT,
72
+ min: 0,
73
+ max: 100,
95
74
  },
96
75
  {
97
76
  key: "userMetric.min_debt_interest_rate",
98
77
  label: "Min Debt Interest Rate",
99
78
  type: "number",
100
79
  unit: "percent",
101
- operators: [
102
- "equal",
103
- "less",
104
- "less_or_equal",
105
- "greater",
106
- "greater_or_equal",
107
- ],
80
+ operators: NUMERIC_OPERATORS_DEFAULT,
81
+ min: 0,
82
+ max: 100,
108
83
  },
109
84
  ];
110
85
 
111
- export const ALL_NUMERIC_OPERATORS: AlertQueryOperator[] = [
112
- "equal",
113
- "not_equal",
114
- "less",
115
- "less_or_equal",
116
- "greater",
117
- "greater_or_equal",
118
- "between",
119
- ];
86
+ export const ALL_NUMERIC_OPERATORS: AlertQueryOperator[] =
87
+ NUMERIC_OPERATORS_DEFAULT;
120
88
 
121
89
  export const BOOLEAN_OPERATORS: AlertQueryOperator[] = ["equal"];
122
90
 
123
91
  export const OPERATOR_LABELS: Record<AlertQueryOperator, string> = {
124
92
  equal: "=",
125
- not_equal: "≠",
126
93
  less: "<",
127
94
  less_or_equal: "≤",
128
95
  greater: ">",
129
96
  greater_or_equal: "≥",
130
- between: "between",
131
97
  };
132
98
 
133
99
  export const SEVERITY_LABELS = {
134
- INSIGHT: "Insight",
100
+ HEALTHY: "Healthy",
135
101
  WATCH: "Watch",
136
102
  NEED_ACTION: "Need Action",
137
103
  } as const;
@@ -139,43 +105,32 @@ export const SEVERITY_LABELS = {
139
105
  // react-awesome-query-builder config — drives ImmutableTree state.
140
106
  export const QB_CONFIG: Config = {
141
107
  ...BasicConfig,
108
+ settings: {
109
+ ...BasicConfig.settings,
110
+ maxNesting: 3,
111
+ maxNumberOfRules: 5,
112
+ },
142
113
  fields: {
143
114
  "userMetric.max_loan_amount": {
144
115
  label: "Borrowing Capacity",
145
116
  type: "number",
146
- operators: [
147
- "equal",
148
- "less",
149
- "less_or_equal",
150
- "greater",
151
- "greater_or_equal",
152
- "between",
153
- ],
117
+ operators: NUMERIC_OPERATORS_DEFAULT,
154
118
  valueSources: ["value"],
119
+ fieldSettings: { min: 0 },
155
120
  },
156
121
  "userMetric.debt_outstanding": {
157
122
  label: "Outstanding Debt",
158
123
  type: "number",
159
- operators: [
160
- "equal",
161
- "less",
162
- "less_or_equal",
163
- "greater",
164
- "greater_or_equal",
165
- ],
124
+ operators: NUMERIC_OPERATORS_DEFAULT,
166
125
  valueSources: ["value"],
126
+ fieldSettings: { min: 0 },
167
127
  },
168
128
  "userMetric.lvr": {
169
129
  label: "Current LVR",
170
130
  type: "number",
171
- operators: [
172
- "equal",
173
- "less",
174
- "less_or_equal",
175
- "greater",
176
- "greater_or_equal",
177
- ],
131
+ operators: NUMERIC_OPERATORS_DEFAULT,
178
132
  valueSources: ["value"],
133
+ fieldSettings: { min: 0, max: 100 },
179
134
  },
180
135
  "userMetric.has_met_buying_goal": {
181
136
  label: "Has Met Buying Goal",
@@ -186,50 +141,30 @@ export const QB_CONFIG: Config = {
186
141
  "userMetric.excess_monthly_surplus": {
187
142
  label: "Excess Monthly Surplus",
188
143
  type: "number",
189
- operators: [
190
- "equal",
191
- "less",
192
- "less_or_equal",
193
- "greater",
194
- "greater_or_equal",
195
- ],
144
+ operators: NUMERIC_OPERATORS_DEFAULT,
196
145
  valueSources: ["value"],
146
+ fieldSettings: { min: 0 },
197
147
  },
198
148
  "userMetric.equity": {
199
149
  label: "Equity Amount",
200
150
  type: "number",
201
- operators: [
202
- "equal",
203
- "less",
204
- "less_or_equal",
205
- "greater",
206
- "greater_or_equal",
207
- ],
151
+ operators: NUMERIC_OPERATORS_DEFAULT,
208
152
  valueSources: ["value"],
153
+ fieldSettings: { min: 0 },
209
154
  },
210
155
  "userMetric.max_debt_interest_rate": {
211
156
  label: "Max Debt Interest Rate",
212
157
  type: "number",
213
- operators: [
214
- "equal",
215
- "less",
216
- "less_or_equal",
217
- "greater",
218
- "greater_or_equal",
219
- ],
158
+ operators: NUMERIC_OPERATORS_DEFAULT,
220
159
  valueSources: ["value"],
160
+ fieldSettings: { min: 0, max: 100 },
221
161
  },
222
162
  "userMetric.min_debt_interest_rate": {
223
163
  label: "Min Debt Interest Rate",
224
164
  type: "number",
225
- operators: [
226
- "equal",
227
- "less",
228
- "less_or_equal",
229
- "greater",
230
- "greater_or_equal",
231
- ],
165
+ operators: NUMERIC_OPERATORS_DEFAULT,
232
166
  valueSources: ["value"],
167
+ fieldSettings: { min: 0, max: 100 },
233
168
  },
234
169
  },
235
170
  };
@@ -88,6 +88,7 @@ export function ContactAlertDialog({
88
88
  isCompanyAdmin = false,
89
89
  initialShareAcrossCompany = false,
90
90
  onSave,
91
+ onError,
91
92
  isLoading = false,
92
93
  className,
93
94
  }: ContactAlertDialogProps) {
@@ -116,10 +117,17 @@ export function ContactAlertDialog({
116
117
 
117
118
  function handleSave() {
118
119
  if (!canSave) return;
120
+ let filterSegment: ImmutableTree;
121
+ try {
122
+ filterSegment = QbUtils.sanitizeTree(tree, QB_CONFIG).fixedTree;
123
+ } catch (e) {
124
+ onError?.(e instanceof Error ? e : new Error(String(e)));
125
+ return;
126
+ }
119
127
  onSave({
120
128
  name: name.trim(),
121
129
  severity,
122
- filterSegment: QbUtils.sanitizeTree(tree, QB_CONFIG).fixedTree,
130
+ filterSegment,
123
131
  sharingType: shareAcrossCompany
124
132
  ? AlertSharingType.COMPANY
125
133
  : AlertSharingType.PRIVATE,
@@ -151,7 +159,7 @@ export function ContactAlertDialog({
151
159
  }
152
160
  >
153
161
  {(
154
- ["NEED_ACTION", "WATCH", "INSIGHT"] as ContactAlertSeverity[]
162
+ ["NEED_ACTION", "WATCH", "HEALTHY"] as ContactAlertSeverity[]
155
163
  ).map((s) => (
156
164
  <ToggleGroupItem key={s} value={s}>
157
165
  {SEVERITY_LABELS[s]}
@@ -4,16 +4,14 @@ export type AlertQueryCombinator = "AND" | "OR";
4
4
 
5
5
  export type AlertQueryOperator =
6
6
  | "equal"
7
- | "not_equal"
8
7
  | "less"
9
8
  | "less_or_equal"
10
9
  | "greater"
11
- | "greater_or_equal"
12
- | "between";
10
+ | "greater_or_equal";
13
11
 
14
12
  export type AlertQueryFieldType = "number" | "boolean";
15
13
 
16
- export type ContactAlertSeverity = "INSIGHT" | "WATCH" | "NEED_ACTION";
14
+ export type ContactAlertSeverity = "HEALTHY" | "WATCH" | "NEED_ACTION";
17
15
 
18
16
  export enum AlertSharingType {
19
17
  PRIVATE = "PRIVATE",
@@ -28,6 +26,10 @@ export interface AlertQueryField {
28
26
  unit?: "dollar" | "percent";
29
27
  /** Allowed operators (defaults to all numeric operators). */
30
28
  operators?: AlertQueryOperator[];
29
+ /** Lower bound for numeric value (inclusive). Enforced at input time. */
30
+ min?: number;
31
+ /** Upper bound for numeric value (inclusive). Enforced at input time. */
32
+ max?: number;
31
33
  }
32
34
 
33
35
  export interface ContactAlertQueryBuilderProps {
@@ -57,5 +59,7 @@ export interface ContactAlertDialogProps {
57
59
  sharingType: AlertSharingType;
58
60
  }) => void;
59
61
  isLoading?: boolean;
62
+ /** Fired when the underlying QB sanitizeTree throws while saving. */
63
+ onError?: (error: Error) => void;
60
64
  className?: string;
61
65
  }
@@ -45,7 +45,6 @@ export function ruleSummary(
45
45
  const field: string = ruleProps?.field ?? "";
46
46
  const operator: string = ruleProps?.operator ?? "equal";
47
47
  const value0 = ruleProps?.value?.[0];
48
- const value1 = ruleProps?.value?.[1];
49
48
 
50
49
  const fieldDef = fields.find((f) => f.key === field);
51
50
  const fieldLabel = fieldDef?.label ?? field;
@@ -62,9 +61,6 @@ export function ruleSummary(
62
61
  return String(v);
63
62
  };
64
63
 
65
- if (operator === "between") {
66
- return `${fieldLabel} between ${formatVal(value0)} and ${formatVal(value1)}`;
67
- }
68
64
  return `${fieldLabel} ${opLabel} ${formatVal(value0)}`;
69
65
  }
70
66
 
@@ -29,6 +29,8 @@ export interface CsvImportModalProps {
29
29
  onNext: () => void;
30
30
  /** Called when the user clicks the CSV template download link. */
31
31
  onDownloadTemplate?: () => void;
32
+ /** Override the upload zone description (e.g. for a stricter file-size cap). */
33
+ uploadDescription?: string;
32
34
  isLoading?: boolean;
33
35
  className?: string;
34
36
  }
@@ -55,6 +57,7 @@ export function CsvImportModal({
55
57
  onFileClear,
56
58
  onNext,
57
59
  onDownloadTemplate,
60
+ uploadDescription,
58
61
  isLoading = false,
59
62
  className,
60
63
  }: CsvImportModalProps) {
@@ -93,7 +96,7 @@ export function CsvImportModal({
93
96
  <UploadCard
94
97
  size="lg"
95
98
  label="Drag & drop or click to upload"
96
- description="Supports .csv files only. Max 10 MB."
99
+ description={uploadDescription ?? "Supports .csv files only. Max 10 MB."}
97
100
  accept=".csv"
98
101
  onFileChange={(file) => file && onFileSelect?.(file)}
99
102
  />
@@ -99,14 +99,21 @@ export interface FilePreviewDialogProps {
99
99
  pageSize?: number;
100
100
  /**
101
101
  * List of staff members available for assignment.
102
- * When provided, a staff selector is rendered above the table.
102
+ * When provided, a basic staff selector is rendered above the table.
103
103
  * Import is blocked until a staff member is selected.
104
+ * Ignored when `staffSelector` is provided.
104
105
  */
105
106
  staffOptions?: StaffOption[];
106
107
  /** Currently selected staff ID. */
107
108
  selectedStaffId?: string;
108
109
  /** Called when the user picks a staff member. */
109
110
  onStaffSelect?: (staffId: string) => void;
111
+ /**
112
+ * Custom staff-selector block rendered above the table. When provided, replaces
113
+ * the entire built-in "Assign staff" block (label + select + helper text).
114
+ * The caller still drives `selectedStaffId` so the Import button gating works.
115
+ */
116
+ staffSelector?: React.ReactNode;
110
117
  className?: string;
111
118
  }
112
119
 
@@ -250,6 +257,7 @@ export function FilePreviewDialog({
250
257
  staffOptions,
251
258
  selectedStaffId,
252
259
  onStaffSelect,
260
+ staffSelector,
253
261
  className,
254
262
  }: FilePreviewDialogProps) {
255
263
  const [page, setPage] = React.useState(0);
@@ -264,7 +272,8 @@ export function FilePreviewDialog({
264
272
  const pageStart = page * pageSize; // used for row index display
265
273
 
266
274
  const isImporting = state === "importing";
267
- const hasStaffSelector = !!staffOptions && staffOptions.length > 0;
275
+ const hasBuiltInStaffSelector = !!staffOptions && staffOptions.length > 0;
276
+ const hasStaffSelector = !!staffSelector || hasBuiltInStaffSelector;
268
277
  // Import is blocked when staff selection is required but none is chosen yet
269
278
  const canImport =
270
279
  state === "preview" &&
@@ -315,36 +324,38 @@ export function FilePreviewDialog({
315
324
 
316
325
  {state === "preview" && (
317
326
  <>
318
- {/* Staff assignment — shown when caller provides staffOptions */}
319
- {hasStaffSelector && (
320
- <div className="flex flex-col gap-1">
321
- <label className="text-label-medium text-foreground">
322
- Assign staff{" "}
323
- <span className="text-destructive" aria-hidden="true">
324
- *
325
- </span>
326
- </label>
327
- <Select
328
- value={selectedStaffId ?? ""}
329
- onValueChange={(id) => onStaffSelect?.(id)}
330
- >
331
- <SelectTrigger className="w-full">
332
- <SelectValue placeholder="Select a staff member" />
333
- </SelectTrigger>
334
- <SelectContent>
335
- {staffOptions!.map((s) => (
336
- <SelectItem key={s.id} value={s.id}>
337
- {s.name}
338
- </SelectItem>
339
- ))}
340
- </SelectContent>
341
- </Select>
342
- <p className="text-xs text-muted-foreground">
343
- All contacts in this import will be assigned to the selected
344
- staff member.
345
- </p>
346
- </div>
347
- )}
327
+ {/* Staff assignment — custom slot wins; otherwise fall back to built-in select */}
328
+ {staffSelector
329
+ ? staffSelector
330
+ : hasBuiltInStaffSelector && (
331
+ <div className="flex flex-col gap-1">
332
+ <label className="text-label-medium text-foreground">
333
+ Assign staff{" "}
334
+ <span className="text-destructive" aria-hidden="true">
335
+ *
336
+ </span>
337
+ </label>
338
+ <Select
339
+ value={selectedStaffId ?? ""}
340
+ onValueChange={(id) => onStaffSelect?.(id)}
341
+ >
342
+ <SelectTrigger className="w-full">
343
+ <SelectValue placeholder="Select a staff member" />
344
+ </SelectTrigger>
345
+ <SelectContent>
346
+ {staffOptions!.map((s) => (
347
+ <SelectItem key={s.id} value={s.id}>
348
+ {s.name}
349
+ </SelectItem>
350
+ ))}
351
+ </SelectContent>
352
+ </Select>
353
+ <p className="text-xs text-muted-foreground">
354
+ All contacts in this import will be assigned to the
355
+ selected staff member.
356
+ </p>
357
+ </div>
358
+ )}
348
359
 
349
360
  <div className="max-h-[360px] overflow-auto border border-border">
350
361
  <Table>