@handled-ai/design-system 0.18.44 → 0.18.46
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/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-why-chips.d.ts +2 -2
- package/dist/components/signal-feedback-inline.d.ts +28 -12
- package/dist/components/signal-feedback-inline.js +130 -16
- package/dist/components/signal-feedback-inline.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +2 -2
- package/dist/components/tabs.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/prototype/index.d.ts +2 -2
- package/dist/prototype/prototype-accounts-view.d.ts +2 -2
- package/dist/prototype/prototype-admin-view.d.ts +2 -2
- package/dist/prototype/prototype-config.d.ts +2 -2
- package/dist/prototype/prototype-inbox-view.d.ts +3 -3
- package/dist/prototype/prototype-inbox-view.js +1 -1
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +2 -2
- package/dist/prototype/prototype-shell.d.ts +2 -2
- package/dist/{signal-priority-popover-B5b-XZ7i.d.ts → signal-priority-popover-DM02Eg_F.d.ts} +2 -2
- package/package.json +1 -1
- package/src/components/__tests__/case-panel-why.test.tsx +59 -0
- package/src/components/signal-feedback-inline.tsx +152 -21
- package/src/prototype/prototype-config.ts +1 -1
- package/src/prototype/prototype-inbox-view.tsx +3 -3
|
@@ -149,4 +149,63 @@ describe("CasePanelSignalApprovalActions", () => {
|
|
|
149
149
|
expect(screen.getByText(/this will approve this action for/i)).toBeInTheDocument()
|
|
150
150
|
expect(screen.getByRole("button", { name: /confirm/i }).className).toContain("h-7")
|
|
151
151
|
})
|
|
152
|
+
|
|
153
|
+
it("lets callers collect edited opportunity preview fields before approval", async () => {
|
|
154
|
+
const onApprove = vi.fn()
|
|
155
|
+
|
|
156
|
+
renderApprovalActions({
|
|
157
|
+
initialApprovalState: "confirming",
|
|
158
|
+
onApprove,
|
|
159
|
+
opportunityPreview: {
|
|
160
|
+
name: "Churn Risk - Northwind Systems",
|
|
161
|
+
accountName: "Northwind Systems",
|
|
162
|
+
stage: "Prospecting",
|
|
163
|
+
closeDate: "Jun 30, 2026",
|
|
164
|
+
closeDateValue: "2026-06-30",
|
|
165
|
+
amount: "$75,000",
|
|
166
|
+
amountValue: 75000,
|
|
167
|
+
description: "Initial description",
|
|
168
|
+
churnType: "Churn Risk",
|
|
169
|
+
churnTypeOptions: ["Churn Risk", { value: "Win Back", label: "Win-back" }],
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
fireEvent.change(screen.getByLabelText("Close Date"), { target: { value: "2026-07-15" } })
|
|
174
|
+
expect(screen.getByLabelText("Amount")).toHaveValue("$75,000")
|
|
175
|
+
fireEvent.change(screen.getByLabelText("Amount"), { target: { value: "90000" } })
|
|
176
|
+
fireEvent.change(screen.getByLabelText("Churn Type"), { target: { value: "Win Back" } })
|
|
177
|
+
fireEvent.change(screen.getByLabelText("Description"), { target: { value: "Updated before create" } })
|
|
178
|
+
fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
|
|
179
|
+
|
|
180
|
+
expect(onApprove).toHaveBeenCalledWith({
|
|
181
|
+
closeDate: "2026-07-15",
|
|
182
|
+
amount: "90000",
|
|
183
|
+
churnType: "Win Back",
|
|
184
|
+
description: "Updated before create",
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("blocks confirmation until the close date is valid", () => {
|
|
189
|
+
const onApprove = vi.fn()
|
|
190
|
+
|
|
191
|
+
renderApprovalActions({
|
|
192
|
+
initialApprovalState: "confirming",
|
|
193
|
+
onApprove,
|
|
194
|
+
opportunityPreview: {
|
|
195
|
+
name: "Churn Risk - Northwind Systems",
|
|
196
|
+
accountName: "Northwind Systems",
|
|
197
|
+
stage: "Prospecting",
|
|
198
|
+
closeDate: "Jun 30, 2026",
|
|
199
|
+
closeDateValue: "2026-06-30",
|
|
200
|
+
amount: "$75,000",
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
fireEvent.change(screen.getByLabelText("Close Date"), { target: { value: "" } })
|
|
205
|
+
expect(screen.getByRole("button", { name: /confirm/i })).toBeDisabled()
|
|
206
|
+
expect(screen.getByText("Enter a valid close date.")).toBeInTheDocument()
|
|
207
|
+
|
|
208
|
+
fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
|
|
209
|
+
expect(onApprove).not.toHaveBeenCalled()
|
|
210
|
+
})
|
|
152
211
|
})
|
|
@@ -73,6 +73,32 @@ const approveReasons = [
|
|
|
73
73
|
|
|
74
74
|
type ApprovalState = "pending" | "confirming" | "creating" | "approving-feedback" | "dismissing" | "approved" | "dismissed" | "auto-approved"
|
|
75
75
|
|
|
76
|
+
interface OpportunityPreviewOption {
|
|
77
|
+
value: string
|
|
78
|
+
label: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface OpportunityPreview {
|
|
82
|
+
name: string
|
|
83
|
+
stage: string
|
|
84
|
+
closeDate: string
|
|
85
|
+
closeDateValue?: string
|
|
86
|
+
amount: string
|
|
87
|
+
/** Raw draft input value. Numeric values render as currency in the editable field. */
|
|
88
|
+
amountValue?: string | number | null
|
|
89
|
+
accountName: string
|
|
90
|
+
description?: string | null
|
|
91
|
+
churnType?: string | null
|
|
92
|
+
churnTypeOptions?: Array<string | OpportunityPreviewOption>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface OpportunityDraft {
|
|
96
|
+
closeDate: string
|
|
97
|
+
amount: string
|
|
98
|
+
description: string
|
|
99
|
+
churnType: string
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
interface SignalApprovalLabels {
|
|
77
103
|
approveButton?: string
|
|
78
104
|
dismissButton?: string
|
|
@@ -106,9 +132,9 @@ interface SignalApprovalContextValue {
|
|
|
106
132
|
labels: Required<SignalApprovalLabels>
|
|
107
133
|
hideApproveButton?: boolean
|
|
108
134
|
approveButtonIconUrl?: string
|
|
109
|
-
opportunityPreview?:
|
|
135
|
+
opportunityPreview?: OpportunityPreview
|
|
110
136
|
requestingApproval: boolean
|
|
111
|
-
approve: () => void
|
|
137
|
+
approve: (draft?: OpportunityDraft) => void
|
|
112
138
|
submitApproveFeedback: (reasons: string[], detail: string) => void
|
|
113
139
|
skipApproveFeedback: () => void
|
|
114
140
|
dismiss: (reasons: string[], detail: string, subReason?: string) => void
|
|
@@ -137,13 +163,7 @@ interface RootProps {
|
|
|
137
163
|
/** Optional icon URL for the approve button. Renders an img instead of CirclePlus when provided. */
|
|
138
164
|
approveButtonIconUrl?: string
|
|
139
165
|
/** Optional structured preview data shown in the confirmation dialog. */
|
|
140
|
-
opportunityPreview?:
|
|
141
|
-
name: string
|
|
142
|
-
stage: string
|
|
143
|
-
closeDate: string
|
|
144
|
-
amount: string
|
|
145
|
-
accountName: string
|
|
146
|
-
}
|
|
166
|
+
opportunityPreview?: OpportunityPreview
|
|
147
167
|
/**
|
|
148
168
|
* Async callback fired when the user clicks the approve button, BEFORE
|
|
149
169
|
* transitioning to the "confirming" state. While the promise is pending,
|
|
@@ -160,7 +180,7 @@ interface RootProps {
|
|
|
160
180
|
* "creating" loading state while the promise is pending. On `true` it
|
|
161
181
|
* transitions to the feedback step; on `false` it reverts to "pending".
|
|
162
182
|
*/
|
|
163
|
-
onApprove?: () => void | Promise<boolean>
|
|
183
|
+
onApprove?: (draft?: OpportunityDraft) => void | Promise<boolean>
|
|
164
184
|
onApproveFeedback?: (reasons: string[], detail: string) => void
|
|
165
185
|
onDismiss?: (reasons: string[], detail: string, subReason?: string) => void
|
|
166
186
|
}
|
|
@@ -205,8 +225,8 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
|
|
|
205
225
|
setApprovalState("pending")
|
|
206
226
|
}, [])
|
|
207
227
|
|
|
208
|
-
const approve = React.useCallback(() => {
|
|
209
|
-
const result = onApprove?.()
|
|
228
|
+
const approve = React.useCallback((draft?: OpportunityDraft) => {
|
|
229
|
+
const result = onApprove?.(draft)
|
|
210
230
|
// If the callback returns a Promise, show a loading state and wait for it.
|
|
211
231
|
if (result && typeof (result as Promise<boolean>).then === "function") {
|
|
212
232
|
setApprovalState("creating")
|
|
@@ -417,6 +437,43 @@ function SubmittedFeedback({
|
|
|
417
437
|
)
|
|
418
438
|
}
|
|
419
439
|
|
|
440
|
+
function optionValue(option: string | OpportunityPreviewOption): string {
|
|
441
|
+
return typeof option === "string" ? option : option.value
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function optionLabel(option: string | OpportunityPreviewOption): string {
|
|
445
|
+
return typeof option === "string" ? option : option.label
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function formatAmountDraftValue(value: string | number | null | undefined): string {
|
|
449
|
+
if (value == null || value === "") return ""
|
|
450
|
+
if (typeof value === "number") {
|
|
451
|
+
return new Intl.NumberFormat("en-US", {
|
|
452
|
+
style: "currency",
|
|
453
|
+
currency: "USD",
|
|
454
|
+
maximumFractionDigits: 0,
|
|
455
|
+
}).format(value)
|
|
456
|
+
}
|
|
457
|
+
return value
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildOpportunityDraft(preview?: OpportunityPreview): OpportunityDraft {
|
|
461
|
+
return {
|
|
462
|
+
closeDate: preview?.closeDateValue ?? preview?.closeDate ?? "",
|
|
463
|
+
amount: preview?.amountValue == null ? "" : formatAmountDraftValue(preview.amountValue),
|
|
464
|
+
description: preview?.description ?? "",
|
|
465
|
+
churnType: preview?.churnType ?? "",
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isValidDateInput(value: string): boolean {
|
|
470
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
|
471
|
+
if (!match) return false
|
|
472
|
+
const parsed = new Date(`${value}T00:00:00Z`)
|
|
473
|
+
if (Number.isNaN(parsed.getTime())) return false
|
|
474
|
+
return parsed.toISOString().slice(0, 10) === value
|
|
475
|
+
}
|
|
476
|
+
|
|
420
477
|
function Actions() {
|
|
421
478
|
const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
|
|
422
479
|
useSignalApproval()
|
|
@@ -426,6 +483,16 @@ function Actions() {
|
|
|
426
483
|
const [detailText, setDetailText] = React.useState("")
|
|
427
484
|
const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
|
|
428
485
|
const [isEditing, setIsEditing] = React.useState(false)
|
|
486
|
+
const [opportunityDraft, setOpportunityDraft] = React.useState<OpportunityDraft>(() => buildOpportunityDraft(opportunityPreview))
|
|
487
|
+
|
|
488
|
+
React.useEffect(() => {
|
|
489
|
+
if (approvalState === "confirming") {
|
|
490
|
+
setOpportunityDraft(buildOpportunityDraft(opportunityPreview))
|
|
491
|
+
}
|
|
492
|
+
}, [approvalState, opportunityPreview])
|
|
493
|
+
|
|
494
|
+
const churnTypeOptions = opportunityPreview?.churnTypeOptions ?? []
|
|
495
|
+
const hasChurnTypeOptions = churnTypeOptions.length > 0
|
|
429
496
|
|
|
430
497
|
const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
|
|
431
498
|
const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
|
|
@@ -438,6 +505,7 @@ function Actions() {
|
|
|
438
505
|
(!needsText || detailText.trim().length > 0)
|
|
439
506
|
|
|
440
507
|
const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
|
|
508
|
+
const canConfirmOpportunity = !opportunityPreview || isValidDateInput(opportunityDraft.closeDate)
|
|
441
509
|
|
|
442
510
|
const selectTopReason = (label: string) => {
|
|
443
511
|
if (selectedTopReason === label) {
|
|
@@ -740,27 +808,91 @@ function Actions() {
|
|
|
740
808
|
{labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
|
|
741
809
|
</p>
|
|
742
810
|
{opportunityPreview && (
|
|
743
|
-
<div className="mt-3 space-y-
|
|
811
|
+
<div className="mt-3 space-y-3 border-t border-border/50 pt-3">
|
|
744
812
|
{[
|
|
745
813
|
{ label: "Opportunity", value: opportunityPreview.name },
|
|
746
814
|
{ label: "Account", value: opportunityPreview.accountName },
|
|
747
815
|
{ label: "Stage", value: opportunityPreview.stage },
|
|
748
|
-
{ label: "Close Date", value: opportunityPreview.closeDate },
|
|
749
|
-
{ label: "Amount", value: opportunityPreview.amount },
|
|
750
816
|
].map(({ label, value }) => (
|
|
751
|
-
<div key={label} className="flex items-center justify-between text-xs">
|
|
817
|
+
<div key={label} className="flex items-center justify-between gap-3 text-xs">
|
|
752
818
|
<span className="text-muted-foreground">{label}</span>
|
|
753
|
-
<span className="font-medium text-foreground">{value}</span>
|
|
819
|
+
<span className="text-right font-medium text-foreground">{value}</span>
|
|
754
820
|
</div>
|
|
755
821
|
))}
|
|
822
|
+
|
|
823
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
824
|
+
<label className="space-y-1 text-xs">
|
|
825
|
+
<span className="font-medium text-muted-foreground">Close Date</span>
|
|
826
|
+
<input
|
|
827
|
+
type="date"
|
|
828
|
+
value={opportunityDraft.closeDate}
|
|
829
|
+
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, closeDate: event.target.value }))}
|
|
830
|
+
aria-invalid={!canConfirmOpportunity}
|
|
831
|
+
className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
832
|
+
/>
|
|
833
|
+
{!canConfirmOpportunity && (
|
|
834
|
+
<span className="text-[11px] text-red-600">Enter a valid close date.</span>
|
|
835
|
+
)}
|
|
836
|
+
</label>
|
|
837
|
+
|
|
838
|
+
<label className="space-y-1 text-xs">
|
|
839
|
+
<span className="font-medium text-muted-foreground">Amount</span>
|
|
840
|
+
<input
|
|
841
|
+
type="text"
|
|
842
|
+
inputMode="decimal"
|
|
843
|
+
value={opportunityDraft.amount}
|
|
844
|
+
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, amount: event.target.value }))}
|
|
845
|
+
placeholder={opportunityPreview.amount}
|
|
846
|
+
className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
847
|
+
/>
|
|
848
|
+
</label>
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
<label className="space-y-1 text-xs">
|
|
852
|
+
<span className="font-medium text-muted-foreground">Churn Type</span>
|
|
853
|
+
{hasChurnTypeOptions ? (
|
|
854
|
+
<select
|
|
855
|
+
value={opportunityDraft.churnType}
|
|
856
|
+
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
|
|
857
|
+
className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
858
|
+
>
|
|
859
|
+
<option value="">No churn type</option>
|
|
860
|
+
{churnTypeOptions.map((option) => (
|
|
861
|
+
<option key={optionValue(option)} value={optionValue(option)}>
|
|
862
|
+
{optionLabel(option)}
|
|
863
|
+
</option>
|
|
864
|
+
))}
|
|
865
|
+
</select>
|
|
866
|
+
) : (
|
|
867
|
+
<input
|
|
868
|
+
type="text"
|
|
869
|
+
value={opportunityDraft.churnType}
|
|
870
|
+
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
|
|
871
|
+
placeholder="No churn type"
|
|
872
|
+
className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
873
|
+
/>
|
|
874
|
+
)}
|
|
875
|
+
</label>
|
|
876
|
+
|
|
877
|
+
<label className="space-y-1 text-xs">
|
|
878
|
+
<span className="font-medium text-muted-foreground">Description</span>
|
|
879
|
+
<textarea
|
|
880
|
+
value={opportunityDraft.description}
|
|
881
|
+
onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, description: event.target.value }))}
|
|
882
|
+
rows={3}
|
|
883
|
+
placeholder="Add a short description"
|
|
884
|
+
className="w-full resize-none rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
885
|
+
/>
|
|
886
|
+
</label>
|
|
756
887
|
</div>
|
|
757
888
|
)}
|
|
758
889
|
</div>
|
|
759
890
|
<div className="flex items-center gap-2">
|
|
760
891
|
<button
|
|
761
892
|
type="button"
|
|
762
|
-
onClick={approve}
|
|
763
|
-
|
|
893
|
+
onClick={() => approve(opportunityDraft)}
|
|
894
|
+
disabled={!canConfirmOpportunity}
|
|
895
|
+
className="inline-flex h-7 items-center gap-1.5 rounded-md bg-foreground px-3 text-xs font-semibold text-background transition-colors hover:bg-foreground/90 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground"
|
|
764
896
|
>
|
|
765
897
|
<Check className="h-3 w-3" />
|
|
766
898
|
Confirm
|
|
@@ -861,5 +993,4 @@ export {
|
|
|
861
993
|
Gate as SignalApprovalGate,
|
|
862
994
|
}
|
|
863
995
|
export const SignalApproval = { Root, Actions, Gate }
|
|
864
|
-
export type OpportunityPreview
|
|
865
|
-
export type { ApprovalState, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
|
|
996
|
+
export type { ApprovalState, OpportunityPreview, OpportunityDraft, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
|
|
@@ -180,7 +180,7 @@ export interface InboxViewConfig {
|
|
|
180
180
|
quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
|
|
181
181
|
hideAccountsButton?: boolean
|
|
182
182
|
accountDetailsLabel?: string
|
|
183
|
-
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
|
|
183
|
+
onSignalApprove?: (item: QueueItem, draft?: import("../components/signal-feedback-inline").OpportunityDraft) => void | Promise<boolean>
|
|
184
184
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
|
|
185
185
|
signalLabels?: {
|
|
186
186
|
approveButton?: string
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type InboxFilterCategory,
|
|
39
39
|
} from "../components/inbox-toolbar"
|
|
40
40
|
import { GroupedListView, type GroupedListGroup } from "../components/item-list"
|
|
41
|
-
import { SignalApproval, type ApprovalState, type OpportunityPreview } from "../components/signal-feedback-inline"
|
|
41
|
+
import { SignalApproval, type ApprovalState, type OpportunityDraft, type OpportunityPreview } from "../components/signal-feedback-inline"
|
|
42
42
|
import { ScoreWhyChips } from "../components/score-why-chips"
|
|
43
43
|
import { SignalPriorityPopover } from "../components/signal-priority-popover"
|
|
44
44
|
import { type SourceDef } from "../components/detail-view"
|
|
@@ -149,7 +149,7 @@ export interface DetailViewProps {
|
|
|
149
149
|
onSuggestedActionFeedback?: (actionId: number | string, feedback: string, actionTitle?: string) => void
|
|
150
150
|
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
151
151
|
onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
152
|
-
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
|
|
152
|
+
onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => void | Promise<boolean>
|
|
153
153
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
|
|
154
154
|
signalLabels?: InboxViewConfig["signalLabels"]
|
|
155
155
|
hideApproveButton?: boolean
|
|
@@ -475,7 +475,7 @@ export function DetailView({
|
|
|
475
475
|
opportunityPreview={opportunityPreview}
|
|
476
476
|
onRequestApproval={onRequestApproval}
|
|
477
477
|
initialApprovalState={getSignalApprovalState?.(item)}
|
|
478
|
-
onApprove={() => onSignalApprove?.(item)}
|
|
478
|
+
onApprove={(draft) => onSignalApprove?.(item, draft)}
|
|
479
479
|
onApproveFeedback={(reasons, detail) => {
|
|
480
480
|
signalData.onApproveFeedback?.(reasons, detail)
|
|
481
481
|
console.log("Approval feedback:", { taskId: item.id, company: item.company, reasons, detail })
|