@wealthx/shadcn 1.5.1 → 1.5.2
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/.turbo/turbo-build.log +118 -118
- package/CHANGELOG.md +6 -0
- package/dist/chunk-G2EWIP2N.mjs +960 -0
- package/dist/{chunk-MHHA7QGO.mjs → chunk-ODO6BUOF.mjs} +1 -1
- package/dist/chunk-PX4M67XQ.mjs +301 -0
- package/dist/{chunk-FYUSF5KO.mjs → chunk-QRVEI6J3.mjs} +1 -1
- package/dist/{chunk-42NEC57Y.mjs → chunk-RAKBWNQH.mjs} +272 -3
- package/dist/components/ui/{contact-alert-dialog.js → contact-alert-dialog/index.js} +1029 -593
- package/dist/components/ui/contact-alert-dialog/index.mjs +31 -0
- package/dist/components/ui/file-preview-dialog.js +407 -100
- package/dist/components/ui/file-preview-dialog.mjs +3 -1
- package/dist/components/ui/kanban-column.js +408 -113
- package/dist/components/ui/kanban-column.mjs +3 -2
- package/dist/components/ui/opportunity-card.js +383 -88
- package/dist/components/ui/opportunity-card.mjs +2 -1
- package/dist/components/ui/pipeline-board.js +424 -129
- package/dist/components/ui/pipeline-board.mjs +4 -3
- package/dist/index.js +3081 -2282
- package/dist/index.mjs +39 -35
- package/dist/styles.css +1 -1
- package/package.json +5 -4
- package/src/components/index.tsx +3 -2
- package/src/components/ui/contact-alert-dialog/builder-ui.tsx +556 -0
- package/src/components/ui/contact-alert-dialog/config.ts +262 -0
- package/src/components/ui/contact-alert-dialog/contact-alert-dialog.tsx +214 -0
- package/src/components/ui/contact-alert-dialog/index.tsx +15 -0
- package/src/components/ui/contact-alert-dialog/types.ts +61 -0
- package/src/components/ui/contact-alert-dialog/utils.ts +93 -0
- package/src/components/ui/file-preview-dialog.tsx +299 -99
- package/src/components/ui/opportunity-card.tsx +328 -1
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +1 -1
- package/dist/chunk-5WMFKQZ6.mjs +0 -180
- package/dist/chunk-Y24TXIFJ.mjs +0 -518
- package/dist/components/ui/contact-alert-dialog.mjs +0 -27
- package/src/components/ui/contact-alert-dialog.tsx +0 -710
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BasicConfig,
|
|
3
|
+
Utils as QbUtils,
|
|
4
|
+
type Config,
|
|
5
|
+
type ImmutableTree,
|
|
6
|
+
type JsonGroup,
|
|
7
|
+
type JsonItem,
|
|
8
|
+
} from "@react-awesome-query-builder/ui";
|
|
9
|
+
import type { AlertQueryField, AlertQueryOperator } from "./types";
|
|
10
|
+
|
|
11
|
+
export const ALERT_QUERY_FIELDS: AlertQueryField[] = [
|
|
12
|
+
{
|
|
13
|
+
key: "userMetric.max_loan_amount",
|
|
14
|
+
label: "Borrowing Capacity",
|
|
15
|
+
type: "number",
|
|
16
|
+
unit: "dollar",
|
|
17
|
+
operators: [
|
|
18
|
+
"equal",
|
|
19
|
+
"less",
|
|
20
|
+
"less_or_equal",
|
|
21
|
+
"greater",
|
|
22
|
+
"greater_or_equal",
|
|
23
|
+
"between",
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: "userMetric.debt_outstanding",
|
|
28
|
+
label: "Outstanding Debt",
|
|
29
|
+
type: "number",
|
|
30
|
+
unit: "dollar",
|
|
31
|
+
operators: [
|
|
32
|
+
"equal",
|
|
33
|
+
"less",
|
|
34
|
+
"less_or_equal",
|
|
35
|
+
"greater",
|
|
36
|
+
"greater_or_equal",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "userMetric.lvr",
|
|
41
|
+
label: "Current LVR",
|
|
42
|
+
type: "number",
|
|
43
|
+
unit: "percent",
|
|
44
|
+
operators: [
|
|
45
|
+
"equal",
|
|
46
|
+
"less",
|
|
47
|
+
"less_or_equal",
|
|
48
|
+
"greater",
|
|
49
|
+
"greater_or_equal",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "userMetric.has_met_buying_goal",
|
|
54
|
+
label: "Has Met Buying Goal",
|
|
55
|
+
type: "boolean",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "userMetric.excess_monthly_surplus",
|
|
59
|
+
label: "Excess Monthly Surplus",
|
|
60
|
+
type: "number",
|
|
61
|
+
unit: "dollar",
|
|
62
|
+
operators: [
|
|
63
|
+
"equal",
|
|
64
|
+
"less",
|
|
65
|
+
"less_or_equal",
|
|
66
|
+
"greater",
|
|
67
|
+
"greater_or_equal",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "userMetric.equity",
|
|
72
|
+
label: "Equity Amount",
|
|
73
|
+
type: "number",
|
|
74
|
+
unit: "dollar",
|
|
75
|
+
operators: [
|
|
76
|
+
"equal",
|
|
77
|
+
"less",
|
|
78
|
+
"less_or_equal",
|
|
79
|
+
"greater",
|
|
80
|
+
"greater_or_equal",
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "userMetric.max_debt_interest_rate",
|
|
85
|
+
label: "Max Debt Interest Rate",
|
|
86
|
+
type: "number",
|
|
87
|
+
unit: "percent",
|
|
88
|
+
operators: [
|
|
89
|
+
"equal",
|
|
90
|
+
"less",
|
|
91
|
+
"less_or_equal",
|
|
92
|
+
"greater",
|
|
93
|
+
"greater_or_equal",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: "userMetric.min_debt_interest_rate",
|
|
98
|
+
label: "Min Debt Interest Rate",
|
|
99
|
+
type: "number",
|
|
100
|
+
unit: "percent",
|
|
101
|
+
operators: [
|
|
102
|
+
"equal",
|
|
103
|
+
"less",
|
|
104
|
+
"less_or_equal",
|
|
105
|
+
"greater",
|
|
106
|
+
"greater_or_equal",
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
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
|
+
];
|
|
120
|
+
|
|
121
|
+
export const BOOLEAN_OPERATORS: AlertQueryOperator[] = ["equal"];
|
|
122
|
+
|
|
123
|
+
export const OPERATOR_LABELS: Record<AlertQueryOperator, string> = {
|
|
124
|
+
equal: "=",
|
|
125
|
+
not_equal: "≠",
|
|
126
|
+
less: "<",
|
|
127
|
+
less_or_equal: "≤",
|
|
128
|
+
greater: ">",
|
|
129
|
+
greater_or_equal: "≥",
|
|
130
|
+
between: "between",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const SEVERITY_LABELS = {
|
|
134
|
+
INSIGHT: "Insight",
|
|
135
|
+
WATCH: "Watch",
|
|
136
|
+
NEED_ACTION: "Need Action",
|
|
137
|
+
} as const;
|
|
138
|
+
|
|
139
|
+
// react-awesome-query-builder config — drives ImmutableTree state.
|
|
140
|
+
export const QB_CONFIG: Config = {
|
|
141
|
+
...BasicConfig,
|
|
142
|
+
fields: {
|
|
143
|
+
"userMetric.max_loan_amount": {
|
|
144
|
+
label: "Borrowing Capacity",
|
|
145
|
+
type: "number",
|
|
146
|
+
operators: [
|
|
147
|
+
"equal",
|
|
148
|
+
"less",
|
|
149
|
+
"less_or_equal",
|
|
150
|
+
"greater",
|
|
151
|
+
"greater_or_equal",
|
|
152
|
+
"between",
|
|
153
|
+
],
|
|
154
|
+
valueSources: ["value"],
|
|
155
|
+
},
|
|
156
|
+
"userMetric.debt_outstanding": {
|
|
157
|
+
label: "Outstanding Debt",
|
|
158
|
+
type: "number",
|
|
159
|
+
operators: [
|
|
160
|
+
"equal",
|
|
161
|
+
"less",
|
|
162
|
+
"less_or_equal",
|
|
163
|
+
"greater",
|
|
164
|
+
"greater_or_equal",
|
|
165
|
+
],
|
|
166
|
+
valueSources: ["value"],
|
|
167
|
+
},
|
|
168
|
+
"userMetric.lvr": {
|
|
169
|
+
label: "Current LVR",
|
|
170
|
+
type: "number",
|
|
171
|
+
operators: [
|
|
172
|
+
"equal",
|
|
173
|
+
"less",
|
|
174
|
+
"less_or_equal",
|
|
175
|
+
"greater",
|
|
176
|
+
"greater_or_equal",
|
|
177
|
+
],
|
|
178
|
+
valueSources: ["value"],
|
|
179
|
+
},
|
|
180
|
+
"userMetric.has_met_buying_goal": {
|
|
181
|
+
label: "Has Met Buying Goal",
|
|
182
|
+
type: "boolean",
|
|
183
|
+
operators: ["equal"],
|
|
184
|
+
valueSources: ["value"],
|
|
185
|
+
},
|
|
186
|
+
"userMetric.excess_monthly_surplus": {
|
|
187
|
+
label: "Excess Monthly Surplus",
|
|
188
|
+
type: "number",
|
|
189
|
+
operators: [
|
|
190
|
+
"equal",
|
|
191
|
+
"less",
|
|
192
|
+
"less_or_equal",
|
|
193
|
+
"greater",
|
|
194
|
+
"greater_or_equal",
|
|
195
|
+
],
|
|
196
|
+
valueSources: ["value"],
|
|
197
|
+
},
|
|
198
|
+
"userMetric.equity": {
|
|
199
|
+
label: "Equity Amount",
|
|
200
|
+
type: "number",
|
|
201
|
+
operators: [
|
|
202
|
+
"equal",
|
|
203
|
+
"less",
|
|
204
|
+
"less_or_equal",
|
|
205
|
+
"greater",
|
|
206
|
+
"greater_or_equal",
|
|
207
|
+
],
|
|
208
|
+
valueSources: ["value"],
|
|
209
|
+
},
|
|
210
|
+
"userMetric.max_debt_interest_rate": {
|
|
211
|
+
label: "Max Debt Interest Rate",
|
|
212
|
+
type: "number",
|
|
213
|
+
operators: [
|
|
214
|
+
"equal",
|
|
215
|
+
"less",
|
|
216
|
+
"less_or_equal",
|
|
217
|
+
"greater",
|
|
218
|
+
"greater_or_equal",
|
|
219
|
+
],
|
|
220
|
+
valueSources: ["value"],
|
|
221
|
+
},
|
|
222
|
+
"userMetric.min_debt_interest_rate": {
|
|
223
|
+
label: "Min Debt Interest Rate",
|
|
224
|
+
type: "number",
|
|
225
|
+
operators: [
|
|
226
|
+
"equal",
|
|
227
|
+
"less",
|
|
228
|
+
"less_or_equal",
|
|
229
|
+
"greater",
|
|
230
|
+
"greater_or_equal",
|
|
231
|
+
],
|
|
232
|
+
valueSources: ["value"],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Default query — children1 in array form (matches JsonGroup type).
|
|
238
|
+
export const EMPTY_QUERY_VALUE: JsonGroup = {
|
|
239
|
+
id: "root",
|
|
240
|
+
type: "group",
|
|
241
|
+
properties: { conjunction: "AND", not: false },
|
|
242
|
+
children1: [
|
|
243
|
+
{
|
|
244
|
+
id: "rule-init",
|
|
245
|
+
type: "rule",
|
|
246
|
+
properties: {
|
|
247
|
+
field: "userMetric.max_loan_amount",
|
|
248
|
+
operator: "greater_or_equal",
|
|
249
|
+
value: [null],
|
|
250
|
+
valueSrc: ["value"],
|
|
251
|
+
},
|
|
252
|
+
} as JsonItem,
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/** Convert a JsonGroup to an ImmutableTree usable by the query builder. */
|
|
257
|
+
export function createAlertTree(query?: JsonGroup | null): ImmutableTree {
|
|
258
|
+
const q = query ?? EMPTY_QUERY_VALUE;
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
const loaded = QbUtils.loadTree(q as any);
|
|
261
|
+
return QbUtils.sanitizeTree(loaded, QB_CONFIG).fixedTree;
|
|
262
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Query,
|
|
4
|
+
Utils as QbUtils,
|
|
5
|
+
type ImmutableTree,
|
|
6
|
+
} from "@react-awesome-query-builder/ui";
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
} from "@/components/ui/dialog";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { Input } from "@/components/ui/input";
|
|
16
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
17
|
+
import { Label } from "@/components/ui/label";
|
|
18
|
+
import { Field, FieldLabel } from "@/components/ui/field";
|
|
19
|
+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
20
|
+
import type {
|
|
21
|
+
ContactAlertDialogProps,
|
|
22
|
+
ContactAlertQueryBuilderProps,
|
|
23
|
+
ContactAlertSeverity,
|
|
24
|
+
} from "./types";
|
|
25
|
+
import { AlertSharingType } from "./types";
|
|
26
|
+
import {
|
|
27
|
+
ALERT_QUERY_FIELDS,
|
|
28
|
+
QB_CONFIG,
|
|
29
|
+
SEVERITY_LABELS,
|
|
30
|
+
createAlertTree,
|
|
31
|
+
} from "./config";
|
|
32
|
+
import { isTreeValid } from "./utils";
|
|
33
|
+
import { CustomBuilderUI } from "./builder-ui";
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// ContactAlertQueryBuilder
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export function ContactAlertQueryBuilder({
|
|
40
|
+
value,
|
|
41
|
+
onChange,
|
|
42
|
+
fields = ALERT_QUERY_FIELDS,
|
|
43
|
+
className,
|
|
44
|
+
}: ContactAlertQueryBuilderProps) {
|
|
45
|
+
const defaultOpenItems = React.useMemo(() => {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
const json = QbUtils.getTree(value) as any;
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
return (json?.children1 ?? []).map((c: any) => c.id ?? "").filter(Boolean);
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const renderBuilder = React.useCallback(
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
(props: any) => (
|
|
56
|
+
<CustomBuilderUI
|
|
57
|
+
tree={props.tree}
|
|
58
|
+
actions={props.actions}
|
|
59
|
+
fields={fields}
|
|
60
|
+
defaultOpenItems={defaultOpenItems}
|
|
61
|
+
className={className}
|
|
62
|
+
/>
|
|
63
|
+
),
|
|
64
|
+
[fields, defaultOpenItems, className],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Query
|
|
69
|
+
{...QB_CONFIG}
|
|
70
|
+
value={value}
|
|
71
|
+
onChange={(newTree) => onChange(newTree)}
|
|
72
|
+
renderBuilder={renderBuilder}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// ContactAlertDialog
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
export function ContactAlertDialog({
|
|
82
|
+
open,
|
|
83
|
+
onOpenChange,
|
|
84
|
+
mode = "create",
|
|
85
|
+
initialName = "",
|
|
86
|
+
initialSeverity = "NEED_ACTION",
|
|
87
|
+
initialQuery,
|
|
88
|
+
isCompanyAdmin = false,
|
|
89
|
+
initialShareAcrossCompany = false,
|
|
90
|
+
onSave,
|
|
91
|
+
isLoading = false,
|
|
92
|
+
className,
|
|
93
|
+
}: ContactAlertDialogProps) {
|
|
94
|
+
const [name, setName] = React.useState(initialName);
|
|
95
|
+
const [severity, setSeverity] =
|
|
96
|
+
React.useState<ContactAlertSeverity>(initialSeverity);
|
|
97
|
+
const [tree, setTree] = React.useState<ImmutableTree>(() =>
|
|
98
|
+
createAlertTree(initialQuery),
|
|
99
|
+
);
|
|
100
|
+
const [shareAcrossCompany, setShareAcrossCompany] = React.useState(
|
|
101
|
+
initialShareAcrossCompany,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
React.useEffect(() => {
|
|
105
|
+
if (open) {
|
|
106
|
+
setName(initialName);
|
|
107
|
+
setSeverity(initialSeverity);
|
|
108
|
+
setTree(createAlertTree(initialQuery));
|
|
109
|
+
setShareAcrossCompany(initialShareAcrossCompany);
|
|
110
|
+
}
|
|
111
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
112
|
+
}, [open]);
|
|
113
|
+
|
|
114
|
+
const hasValidRule = React.useMemo(() => isTreeValid(tree), [tree]);
|
|
115
|
+
const canSave = name.trim().length > 0 && hasValidRule && !isLoading;
|
|
116
|
+
|
|
117
|
+
function handleSave() {
|
|
118
|
+
if (!canSave) return;
|
|
119
|
+
onSave({
|
|
120
|
+
name: name.trim(),
|
|
121
|
+
severity,
|
|
122
|
+
filterSegment: QbUtils.sanitizeTree(tree, QB_CONFIG).fixedTree,
|
|
123
|
+
sharingType: shareAcrossCompany
|
|
124
|
+
? AlertSharingType.COMPANY
|
|
125
|
+
: AlertSharingType.PRIVATE,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Dialog open={open} onOpenChange={isLoading ? undefined : onOpenChange}>
|
|
131
|
+
<DialogContent size="2xl" className={className}>
|
|
132
|
+
<DialogHeader>
|
|
133
|
+
<DialogTitle>
|
|
134
|
+
{mode === "edit" ? "Update Alert" : "Create Alert"}
|
|
135
|
+
</DialogTitle>
|
|
136
|
+
</DialogHeader>
|
|
137
|
+
|
|
138
|
+
{/* Severity */}
|
|
139
|
+
<div className="flex flex-col gap-1.5">
|
|
140
|
+
<p className="text-sm font-medium text-foreground">
|
|
141
|
+
Alert trigger severity
|
|
142
|
+
</p>
|
|
143
|
+
<ToggleGroup
|
|
144
|
+
type="single"
|
|
145
|
+
variant="outline"
|
|
146
|
+
size="sm"
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
value={severity as any}
|
|
149
|
+
onValueChange={(v) =>
|
|
150
|
+
v && setSeverity(v as unknown as ContactAlertSeverity)
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
{(
|
|
154
|
+
["NEED_ACTION", "WATCH", "INSIGHT"] as ContactAlertSeverity[]
|
|
155
|
+
).map((s) => (
|
|
156
|
+
<ToggleGroupItem key={s} value={s}>
|
|
157
|
+
{SEVERITY_LABELS[s]}
|
|
158
|
+
</ToggleGroupItem>
|
|
159
|
+
))}
|
|
160
|
+
</ToggleGroup>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Name */}
|
|
164
|
+
<Field>
|
|
165
|
+
<FieldLabel>Alert name</FieldLabel>
|
|
166
|
+
<Input
|
|
167
|
+
value={name}
|
|
168
|
+
onChange={(e) => setName(e.target.value)}
|
|
169
|
+
placeholder="e.g. High equity opportunity"
|
|
170
|
+
/>
|
|
171
|
+
</Field>
|
|
172
|
+
|
|
173
|
+
{/* Share across company */}
|
|
174
|
+
{isCompanyAdmin && (
|
|
175
|
+
<div className="flex items-center gap-2">
|
|
176
|
+
<Checkbox
|
|
177
|
+
id="alert-share"
|
|
178
|
+
checked={shareAcrossCompany}
|
|
179
|
+
onCheckedChange={(v) => setShareAcrossCompany(!!v)}
|
|
180
|
+
/>
|
|
181
|
+
<Label htmlFor="alert-share" className="cursor-pointer font-normal">
|
|
182
|
+
Share across company
|
|
183
|
+
</Label>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Query builder */}
|
|
188
|
+
<div className="flex flex-col gap-1.5">
|
|
189
|
+
<p className="text-sm font-medium text-foreground">
|
|
190
|
+
Filter conditions
|
|
191
|
+
</p>
|
|
192
|
+
<ContactAlertQueryBuilder
|
|
193
|
+
value={tree}
|
|
194
|
+
onChange={setTree}
|
|
195
|
+
fields={ALERT_QUERY_FIELDS}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<DialogFooter>
|
|
200
|
+
<Button
|
|
201
|
+
variant="outline"
|
|
202
|
+
onClick={() => onOpenChange(false)}
|
|
203
|
+
disabled={isLoading}
|
|
204
|
+
>
|
|
205
|
+
Cancel
|
|
206
|
+
</Button>
|
|
207
|
+
<Button disabled={!canSave} onClick={handleSave}>
|
|
208
|
+
{mode === "edit" ? "Update Alert" : "Create Alert"}
|
|
209
|
+
</Button>
|
|
210
|
+
</DialogFooter>
|
|
211
|
+
</DialogContent>
|
|
212
|
+
</Dialog>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ContactAlertQueryBuilder,
|
|
3
|
+
ContactAlertDialog,
|
|
4
|
+
} from "./contact-alert-dialog";
|
|
5
|
+
export { ALERT_QUERY_FIELDS, createAlertTree } from "./config";
|
|
6
|
+
export { AlertSharingType } from "./types";
|
|
7
|
+
export type {
|
|
8
|
+
AlertQueryCombinator,
|
|
9
|
+
AlertQueryField,
|
|
10
|
+
AlertQueryFieldType,
|
|
11
|
+
AlertQueryOperator,
|
|
12
|
+
ContactAlertDialogProps,
|
|
13
|
+
ContactAlertQueryBuilderProps,
|
|
14
|
+
ContactAlertSeverity,
|
|
15
|
+
} from "./types";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ImmutableTree, JsonGroup } from "@react-awesome-query-builder/ui";
|
|
2
|
+
|
|
3
|
+
export type AlertQueryCombinator = "AND" | "OR";
|
|
4
|
+
|
|
5
|
+
export type AlertQueryOperator =
|
|
6
|
+
| "equal"
|
|
7
|
+
| "not_equal"
|
|
8
|
+
| "less"
|
|
9
|
+
| "less_or_equal"
|
|
10
|
+
| "greater"
|
|
11
|
+
| "greater_or_equal"
|
|
12
|
+
| "between";
|
|
13
|
+
|
|
14
|
+
export type AlertQueryFieldType = "number" | "boolean";
|
|
15
|
+
|
|
16
|
+
export type ContactAlertSeverity = "INSIGHT" | "WATCH" | "NEED_ACTION";
|
|
17
|
+
|
|
18
|
+
export enum AlertSharingType {
|
|
19
|
+
PRIVATE = "PRIVATE",
|
|
20
|
+
COMPANY = "COMPANY",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AlertQueryField {
|
|
24
|
+
key: string;
|
|
25
|
+
label: string;
|
|
26
|
+
type: AlertQueryFieldType;
|
|
27
|
+
/** "dollar" = prefix "$", "percent" = suffix "%" */
|
|
28
|
+
unit?: "dollar" | "percent";
|
|
29
|
+
/** Allowed operators (defaults to all numeric operators). */
|
|
30
|
+
operators?: AlertQueryOperator[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ContactAlertQueryBuilderProps {
|
|
34
|
+
/** Current ImmutableTree state — fully controlled. */
|
|
35
|
+
value: ImmutableTree;
|
|
36
|
+
onChange: (newTree: ImmutableTree) => void;
|
|
37
|
+
fields?: AlertQueryField[];
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ContactAlertDialogProps {
|
|
42
|
+
open: boolean;
|
|
43
|
+
onOpenChange: (open: boolean) => void;
|
|
44
|
+
mode?: "create" | "edit";
|
|
45
|
+
initialName?: string;
|
|
46
|
+
initialSeverity?: ContactAlertSeverity;
|
|
47
|
+
/** Initial query in JsonGroup format (array children1). Converted to ImmutableTree on open. */
|
|
48
|
+
initialQuery?: JsonGroup;
|
|
49
|
+
/** Show "Share across company" checkbox for company admins. */
|
|
50
|
+
isCompanyAdmin?: boolean;
|
|
51
|
+
initialShareAcrossCompany?: boolean;
|
|
52
|
+
onSave: (data: {
|
|
53
|
+
name: string;
|
|
54
|
+
severity: ContactAlertSeverity;
|
|
55
|
+
/** Backend-compatible ImmutableTree (filterSegment format). */
|
|
56
|
+
filterSegment: ImmutableTree;
|
|
57
|
+
sharingType: AlertSharingType;
|
|
58
|
+
}) => void;
|
|
59
|
+
isLoading?: boolean;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Utils as QbUtils,
|
|
3
|
+
type ImmutableTree,
|
|
4
|
+
} from "@react-awesome-query-builder/ui";
|
|
5
|
+
import type { AlertQueryField, AlertQueryOperator } from "./types";
|
|
6
|
+
import {
|
|
7
|
+
ALERT_QUERY_FIELDS,
|
|
8
|
+
ALL_NUMERIC_OPERATORS,
|
|
9
|
+
BOOLEAN_OPERATORS,
|
|
10
|
+
OPERATOR_LABELS,
|
|
11
|
+
} from "./config";
|
|
12
|
+
|
|
13
|
+
export function longestOf(labels: string[]): string {
|
|
14
|
+
return labels.reduce((a, b) => (a.length >= b.length ? a : b), "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function allOperatorLabels(fields: AlertQueryField[]): string[] {
|
|
18
|
+
const ops = new Set<AlertQueryOperator>();
|
|
19
|
+
for (const f of fields) {
|
|
20
|
+
const fieldOps =
|
|
21
|
+
f.type === "boolean"
|
|
22
|
+
? BOOLEAN_OPERATORS
|
|
23
|
+
: (f.operators ?? ALL_NUMERIC_OPERATORS);
|
|
24
|
+
fieldOps.forEach((op) => ops.add(op));
|
|
25
|
+
}
|
|
26
|
+
return Array.from(ops).map((op) => OPERATOR_LABELS[op]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatWithCommas(raw: string): string {
|
|
30
|
+
if (!raw) return "";
|
|
31
|
+
const [int, dec] = raw.split(".");
|
|
32
|
+
const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
33
|
+
return dec !== undefined ? `${formatted}.${dec}` : formatted;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseCommas(display: string): string {
|
|
37
|
+
return display.replace(/[^\d.]/g, "").replace(/(\..*)\./g, "$1");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ruleSummary(
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
ruleProps: any,
|
|
43
|
+
fields: AlertQueryField[],
|
|
44
|
+
): string {
|
|
45
|
+
const field: string = ruleProps?.field ?? "";
|
|
46
|
+
const operator: string = ruleProps?.operator ?? "equal";
|
|
47
|
+
const value0 = ruleProps?.value?.[0];
|
|
48
|
+
const value1 = ruleProps?.value?.[1];
|
|
49
|
+
|
|
50
|
+
const fieldDef = fields.find((f) => f.key === field);
|
|
51
|
+
const fieldLabel = fieldDef?.label ?? field;
|
|
52
|
+
const opLabel = OPERATOR_LABELS[operator as AlertQueryOperator] ?? operator;
|
|
53
|
+
|
|
54
|
+
const formatVal = (v: unknown): string => {
|
|
55
|
+
if (v === null || v === undefined) return "…";
|
|
56
|
+
if (fieldDef?.type === "boolean") return v ? "Yes" : "No";
|
|
57
|
+
const n = typeof v === "number" ? v : parseFloat(String(v));
|
|
58
|
+
if (!isFinite(n)) return "…";
|
|
59
|
+
if (fieldDef?.unit === "dollar")
|
|
60
|
+
return `$${formatWithCommas(n.toString())}`;
|
|
61
|
+
if (fieldDef?.unit === "percent") return `${n}%`;
|
|
62
|
+
return String(v);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (operator === "between") {
|
|
66
|
+
return `${fieldLabel} between ${formatVal(value0)} and ${formatVal(value1)}`;
|
|
67
|
+
}
|
|
68
|
+
return `${fieldLabel} ${opLabel} ${formatVal(value0)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
+
function checkGroupValid(children: any[]): boolean {
|
|
73
|
+
return children.some((child) => {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
const c = child as any;
|
|
76
|
+
if (c.type === "rule") {
|
|
77
|
+
const field: string | undefined = c.properties?.field;
|
|
78
|
+
if (!field) return false;
|
|
79
|
+
const fieldDef = ALERT_QUERY_FIELDS.find((f) => f.key === field);
|
|
80
|
+
if (fieldDef?.type === "boolean") return true;
|
|
81
|
+
const v = c.properties?.value?.[0];
|
|
82
|
+
return v !== null && v !== undefined;
|
|
83
|
+
}
|
|
84
|
+
if (c.type === "group") return checkGroupValid(c.children1 ?? []);
|
|
85
|
+
return false;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isTreeValid(tree: ImmutableTree): boolean {
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
const json = QbUtils.getTree(tree) as any;
|
|
92
|
+
return checkGroupValid(json?.children1 ?? []);
|
|
93
|
+
}
|