@happyvertical/smrt-sales 0.39.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.
Files changed (201) hide show
  1. package/AGENTS.md +87 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/chunks/__smrt-register__-kWdT7LOt.js +6 -0
  8. package/dist/chunks/__smrt-register__-kWdT7LOt.js.map +1 -0
  9. package/dist/chunks/commissions-BCxqOKVM.js +2181 -0
  10. package/dist/chunks/commissions-BCxqOKVM.js.map +1 -0
  11. package/dist/chunks/crm-DwEz7E2r.js +1428 -0
  12. package/dist/chunks/crm-DwEz7E2r.js.map +1 -0
  13. package/dist/chunks/referrals-BY47p70T.js +2880 -0
  14. package/dist/chunks/referrals-BY47p70T.js.map +1 -0
  15. package/dist/commissions/collections/CommissionAdjustmentCollection.d.ts +26 -0
  16. package/dist/commissions/collections/CommissionAdjustmentCollection.d.ts.map +1 -0
  17. package/dist/commissions/collections/CommissionCollection.d.ts +43 -0
  18. package/dist/commissions/collections/CommissionCollection.d.ts.map +1 -0
  19. package/dist/commissions/collections/CommissionPayoutCollection.d.ts +19 -0
  20. package/dist/commissions/collections/CommissionPayoutCollection.d.ts.map +1 -0
  21. package/dist/commissions/collections/CommissionPlanCollection.d.ts +46 -0
  22. package/dist/commissions/collections/CommissionPlanCollection.d.ts.map +1 -0
  23. package/dist/commissions/collections/EarnerCollection.d.ts +14 -0
  24. package/dist/commissions/collections/EarnerCollection.d.ts.map +1 -0
  25. package/dist/commissions/collections/EarningEventCollection.d.ts +27 -0
  26. package/dist/commissions/collections/EarningEventCollection.d.ts.map +1 -0
  27. package/dist/commissions/collections/index.d.ts +11 -0
  28. package/dist/commissions/collections/index.d.ts.map +1 -0
  29. package/dist/commissions/index.d.ts +20 -0
  30. package/dist/commissions/index.d.ts.map +1 -0
  31. package/dist/commissions/models/Commission.d.ts +150 -0
  32. package/dist/commissions/models/Commission.d.ts.map +1 -0
  33. package/dist/commissions/models/CommissionAdjustment.d.ts +62 -0
  34. package/dist/commissions/models/CommissionAdjustment.d.ts.map +1 -0
  35. package/dist/commissions/models/CommissionPayout.d.ts +119 -0
  36. package/dist/commissions/models/CommissionPayout.d.ts.map +1 -0
  37. package/dist/commissions/models/CommissionPlan.d.ts +117 -0
  38. package/dist/commissions/models/CommissionPlan.d.ts.map +1 -0
  39. package/dist/commissions/models/Earner.d.ts +49 -0
  40. package/dist/commissions/models/Earner.d.ts.map +1 -0
  41. package/dist/commissions/models/EarningEvent.d.ts +89 -0
  42. package/dist/commissions/models/EarningEvent.d.ts.map +1 -0
  43. package/dist/commissions/models/index.d.ts +11 -0
  44. package/dist/commissions/models/index.d.ts.map +1 -0
  45. package/dist/commissions/money.d.ts +42 -0
  46. package/dist/commissions/money.d.ts.map +1 -0
  47. package/dist/commissions/services/CommissionBalanceService.d.ts +14 -0
  48. package/dist/commissions/services/CommissionBalanceService.d.ts.map +1 -0
  49. package/dist/commissions/services/CommissionCalculationService.d.ts +142 -0
  50. package/dist/commissions/services/CommissionCalculationService.d.ts.map +1 -0
  51. package/dist/commissions/services/CommissionPayoutService.d.ts +134 -0
  52. package/dist/commissions/services/CommissionPayoutService.d.ts.map +1 -0
  53. package/dist/commissions/services/CommissionSettlementService.d.ts +42 -0
  54. package/dist/commissions/services/CommissionSettlementService.d.ts.map +1 -0
  55. package/dist/commissions/services/index.d.ts +9 -0
  56. package/dist/commissions/services/index.d.ts.map +1 -0
  57. package/dist/commissions/types.d.ts +243 -0
  58. package/dist/commissions/types.d.ts.map +1 -0
  59. package/dist/commissions.d.ts +2 -0
  60. package/dist/commissions.d.ts.map +1 -0
  61. package/dist/commissions.js +3 -0
  62. package/dist/crm/collections/LeadCollection.d.ts +79 -0
  63. package/dist/crm/collections/LeadCollection.d.ts.map +1 -0
  64. package/dist/crm/collections/OpportunityCollection.d.ts +42 -0
  65. package/dist/crm/collections/OpportunityCollection.d.ts.map +1 -0
  66. package/dist/crm/collections/OpportunityConversionCollection.d.ts +26 -0
  67. package/dist/crm/collections/OpportunityConversionCollection.d.ts.map +1 -0
  68. package/dist/crm/collections/PipelineDefinitionCollection.d.ts +46 -0
  69. package/dist/crm/collections/PipelineDefinitionCollection.d.ts.map +1 -0
  70. package/dist/crm/collections/PipelineStageCollection.d.ts +22 -0
  71. package/dist/crm/collections/PipelineStageCollection.d.ts.map +1 -0
  72. package/dist/crm/collections/SalesActivityCollection.d.ts +24 -0
  73. package/dist/crm/collections/SalesActivityCollection.d.ts.map +1 -0
  74. package/dist/crm/collections/SalesRepresentativeCollection.d.ts +19 -0
  75. package/dist/crm/collections/SalesRepresentativeCollection.d.ts.map +1 -0
  76. package/dist/crm/index.d.ts +27 -0
  77. package/dist/crm/index.d.ts.map +1 -0
  78. package/dist/crm/models/Lead.d.ts +135 -0
  79. package/dist/crm/models/Lead.d.ts.map +1 -0
  80. package/dist/crm/models/Opportunity.d.ts +111 -0
  81. package/dist/crm/models/Opportunity.d.ts.map +1 -0
  82. package/dist/crm/models/OpportunityConversion.d.ts +66 -0
  83. package/dist/crm/models/OpportunityConversion.d.ts.map +1 -0
  84. package/dist/crm/models/PipelineDefinition.d.ts +58 -0
  85. package/dist/crm/models/PipelineDefinition.d.ts.map +1 -0
  86. package/dist/crm/models/PipelineStage.d.ts +66 -0
  87. package/dist/crm/models/PipelineStage.d.ts.map +1 -0
  88. package/dist/crm/models/SalesActivity.d.ts +76 -0
  89. package/dist/crm/models/SalesActivity.d.ts.map +1 -0
  90. package/dist/crm/models/SalesRepresentative.d.ts +60 -0
  91. package/dist/crm/models/SalesRepresentative.d.ts.map +1 -0
  92. package/dist/crm/types.d.ts +228 -0
  93. package/dist/crm/types.d.ts.map +1 -0
  94. package/dist/crm.d.ts +2 -0
  95. package/dist/crm.d.ts.map +1 -0
  96. package/dist/crm.js +3 -0
  97. package/dist/index.d.ts +4 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +5 -0
  100. package/dist/manifest.json +9340 -0
  101. package/dist/referrals/collections/AttributionExceptionCollection.d.ts +17 -0
  102. package/dist/referrals/collections/AttributionExceptionCollection.d.ts.map +1 -0
  103. package/dist/referrals/collections/AttributionPolicyCollection.d.ts +50 -0
  104. package/dist/referrals/collections/AttributionPolicyCollection.d.ts.map +1 -0
  105. package/dist/referrals/collections/ReferralAgreementCollection.d.ts +48 -0
  106. package/dist/referrals/collections/ReferralAgreementCollection.d.ts.map +1 -0
  107. package/dist/referrals/collections/ReferralCollection.d.ts +25 -0
  108. package/dist/referrals/collections/ReferralCollection.d.ts.map +1 -0
  109. package/dist/referrals/collections/ReferralLinkCollection.d.ts +96 -0
  110. package/dist/referrals/collections/ReferralLinkCollection.d.ts.map +1 -0
  111. package/dist/referrals/collections/ReferralProgramCollection.d.ts +18 -0
  112. package/dist/referrals/collections/ReferralProgramCollection.d.ts.map +1 -0
  113. package/dist/referrals/collections/ReferralTermSnapshotCollection.d.ts +13 -0
  114. package/dist/referrals/collections/ReferralTermSnapshotCollection.d.ts.map +1 -0
  115. package/dist/referrals/collections/ReferralTouchCollection.d.ts +30 -0
  116. package/dist/referrals/collections/ReferralTouchCollection.d.ts.map +1 -0
  117. package/dist/referrals/collections/ReferrerCollection.d.ts +17 -0
  118. package/dist/referrals/collections/ReferrerCollection.d.ts.map +1 -0
  119. package/dist/referrals/index.d.ts +38 -0
  120. package/dist/referrals/index.d.ts.map +1 -0
  121. package/dist/referrals/models/AttributionException.d.ts +85 -0
  122. package/dist/referrals/models/AttributionException.d.ts.map +1 -0
  123. package/dist/referrals/models/AttributionPolicy.d.ts +128 -0
  124. package/dist/referrals/models/AttributionPolicy.d.ts.map +1 -0
  125. package/dist/referrals/models/Referral.d.ts +112 -0
  126. package/dist/referrals/models/Referral.d.ts.map +1 -0
  127. package/dist/referrals/models/ReferralAgreement.d.ts +125 -0
  128. package/dist/referrals/models/ReferralAgreement.d.ts.map +1 -0
  129. package/dist/referrals/models/ReferralLink.d.ts +71 -0
  130. package/dist/referrals/models/ReferralLink.d.ts.map +1 -0
  131. package/dist/referrals/models/ReferralProgram.d.ts +57 -0
  132. package/dist/referrals/models/ReferralProgram.d.ts.map +1 -0
  133. package/dist/referrals/models/ReferralTermSnapshot.d.ts +61 -0
  134. package/dist/referrals/models/ReferralTermSnapshot.d.ts.map +1 -0
  135. package/dist/referrals/models/ReferralTouch.d.ts +55 -0
  136. package/dist/referrals/models/ReferralTouch.d.ts.map +1 -0
  137. package/dist/referrals/models/Referrer.d.ts +69 -0
  138. package/dist/referrals/models/Referrer.d.ts.map +1 -0
  139. package/dist/referrals/services/AttributionService.d.ts +248 -0
  140. package/dist/referrals/services/AttributionService.d.ts.map +1 -0
  141. package/dist/referrals/services/ReferralCommissionService.d.ts +91 -0
  142. package/dist/referrals/services/ReferralCommissionService.d.ts.map +1 -0
  143. package/dist/referrals/services/ReferralQualificationService.d.ts +135 -0
  144. package/dist/referrals/services/ReferralQualificationService.d.ts.map +1 -0
  145. package/dist/referrals/types.d.ts +226 -0
  146. package/dist/referrals/types.d.ts.map +1 -0
  147. package/dist/referrals.d.ts +2 -0
  148. package/dist/referrals.d.ts.map +1 -0
  149. package/dist/referrals.js +3 -0
  150. package/dist/smrt-knowledge.json +5501 -0
  151. package/dist/svelte/__tests__/format.test.js +68 -0
  152. package/dist/svelte/__tests__/types.test.js +349 -0
  153. package/dist/svelte/components/AttributionConflictQueue.svelte +361 -0
  154. package/dist/svelte/components/AttributionConflictQueue.svelte.d.ts +15 -0
  155. package/dist/svelte/components/AttributionConflictQueue.svelte.d.ts.map +1 -0
  156. package/dist/svelte/components/CommissionBreakdown.svelte +251 -0
  157. package/dist/svelte/components/CommissionBreakdown.svelte.d.ts +11 -0
  158. package/dist/svelte/components/CommissionBreakdown.svelte.d.ts.map +1 -0
  159. package/dist/svelte/components/CommissionExpenseSummary.svelte +186 -0
  160. package/dist/svelte/components/CommissionExpenseSummary.svelte.d.ts +13 -0
  161. package/dist/svelte/components/CommissionExpenseSummary.svelte.d.ts.map +1 -0
  162. package/dist/svelte/components/ExecutedAgreementsList.svelte +136 -0
  163. package/dist/svelte/components/ExecutedAgreementsList.svelte.d.ts +11 -0
  164. package/dist/svelte/components/ExecutedAgreementsList.svelte.d.ts.map +1 -0
  165. package/dist/svelte/components/LeadList.svelte +224 -0
  166. package/dist/svelte/components/LeadList.svelte.d.ts +19 -0
  167. package/dist/svelte/components/LeadList.svelte.d.ts.map +1 -0
  168. package/dist/svelte/components/OpportunityBoard.svelte +230 -0
  169. package/dist/svelte/components/OpportunityBoard.svelte.d.ts +17 -0
  170. package/dist/svelte/components/OpportunityBoard.svelte.d.ts.map +1 -0
  171. package/dist/svelte/components/OpportunityDetail.svelte +472 -0
  172. package/dist/svelte/components/OpportunityDetail.svelte.d.ts +27 -0
  173. package/dist/svelte/components/OpportunityDetail.svelte.d.ts.map +1 -0
  174. package/dist/svelte/components/PayoutBatchReview.svelte +285 -0
  175. package/dist/svelte/components/PayoutBatchReview.svelte.d.ts +21 -0
  176. package/dist/svelte/components/PayoutBatchReview.svelte.d.ts.map +1 -0
  177. package/dist/svelte/components/PayoutHistoryList.svelte +245 -0
  178. package/dist/svelte/components/PayoutHistoryList.svelte.d.ts +11 -0
  179. package/dist/svelte/components/PayoutHistoryList.svelte.d.ts.map +1 -0
  180. package/dist/svelte/components/ReferralLinkManager.svelte +216 -0
  181. package/dist/svelte/components/ReferralLinkManager.svelte.d.ts +17 -0
  182. package/dist/svelte/components/ReferralLinkManager.svelte.d.ts.map +1 -0
  183. package/dist/svelte/components/ReferralStatusList.svelte +118 -0
  184. package/dist/svelte/components/ReferralStatusList.svelte.d.ts +11 -0
  185. package/dist/svelte/components/ReferralStatusList.svelte.d.ts.map +1 -0
  186. package/dist/svelte/components/ReferrerEarningsSummary.svelte +106 -0
  187. package/dist/svelte/components/ReferrerEarningsSummary.svelte.d.ts +11 -0
  188. package/dist/svelte/components/ReferrerEarningsSummary.svelte.d.ts.map +1 -0
  189. package/dist/svelte/components/SalesDashboard.svelte +192 -0
  190. package/dist/svelte/components/SalesDashboard.svelte.d.ts +29 -0
  191. package/dist/svelte/components/SalesDashboard.svelte.d.ts.map +1 -0
  192. package/dist/svelte/format.d.ts +46 -0
  193. package/dist/svelte/format.d.ts.map +1 -0
  194. package/dist/svelte/format.js +75 -0
  195. package/dist/svelte/index.d.ts +50 -0
  196. package/dist/svelte/index.d.ts.map +1 -0
  197. package/dist/svelte/index.js +37 -0
  198. package/dist/svelte/types.d.ts +422 -0
  199. package/dist/svelte/types.d.ts.map +1 -0
  200. package/dist/svelte/types.js +376 -0
  201. package/package.json +89 -0
@@ -0,0 +1,361 @@
1
+ <script lang="ts">
2
+ /**
3
+ * AttributionConflictQueue — operator review queue for attribution
4
+ * exceptions (#1931).
5
+ *
6
+ * Open exceptions show the competing candidate touches and an award editor:
7
+ * per-referrer credit fractions that must sum to 100% (validated inline, the
8
+ * same rule `AttributionService.resolveException` enforces) plus a REQUIRED
9
+ * resolution reason — the resolve button stays disabled until both hold.
10
+ * Resolved exceptions render their read-only audit trail.
11
+ */
12
+ import { FormGroup, Input, Textarea } from '@happyvertical/smrt-ui/forms';
13
+ import { Badge, Button, Card } from '@happyvertical/smrt-ui/ui';
14
+ import { formatDate, formatPercent } from '../format.js';
15
+ import type { AttributionAward, AttributionExceptionView } from '../types.js';
16
+ import {
17
+ equalSplitAwards,
18
+ uniqueCandidateReferrerIds,
19
+ validateAwards,
20
+ } from '../types.js';
21
+
22
+ export interface Props {
23
+ /** Exceptions to review (open and/or resolved). */
24
+ exceptions?: AttributionExceptionView[];
25
+ /** Disable actions while a resolution is in flight. */
26
+ busy?: boolean;
27
+ /** BCP 47 locale for date/percent formatting. */
28
+ locale?: string;
29
+ /** Resolve an open exception with explicit awards and a required reason. */
30
+ onResolve?: (
31
+ exceptionId: string,
32
+ awards: AttributionAward[],
33
+ resolutionReason: string,
34
+ ) => void;
35
+ }
36
+
37
+ let { exceptions = [], busy = false, locale, onResolve }: Props = $props();
38
+
39
+ // Operator edits, keyed by exception id → referrer id → raw input string.
40
+ let fractionDrafts = $state<Record<string, Record<string, string>>>({});
41
+ // Resolution reasons, keyed by exception id.
42
+ let reasonDrafts = $state<Record<string, string>>({});
43
+
44
+ function referrerLabel(
45
+ exception: AttributionExceptionView,
46
+ referrerId: string,
47
+ ): string {
48
+ const candidate = exception.candidates.find(
49
+ (c) => c.referrerId === referrerId,
50
+ );
51
+ return candidate?.referrerName ?? referrerId.slice(0, 8);
52
+ }
53
+
54
+ /** Current award draft: equal-split defaults overlaid with operator edits. */
55
+ function draftAwards(exception: AttributionExceptionView): AttributionAward[] {
56
+ const referrerIds = uniqueCandidateReferrerIds(exception.candidates);
57
+ const defaults = equalSplitAwards(referrerIds);
58
+ const overrides = fractionDrafts[exception.id] ?? {};
59
+ return defaults.map((award) => {
60
+ const raw = overrides[award.referrerId];
61
+ if (raw === undefined) return award;
62
+ return { referrerId: award.referrerId, creditFraction: Number(raw) };
63
+ });
64
+ }
65
+
66
+ function fractionInputValue(
67
+ exception: AttributionExceptionView,
68
+ referrerId: string,
69
+ ): string {
70
+ const raw = fractionDrafts[exception.id]?.[referrerId];
71
+ if (raw !== undefined) return raw;
72
+ const referrerIds = uniqueCandidateReferrerIds(exception.candidates);
73
+ const seeded = equalSplitAwards(referrerIds).find(
74
+ (award) => award.referrerId === referrerId,
75
+ );
76
+ return seeded ? String(seeded.creditFraction) : '';
77
+ }
78
+
79
+ function setFraction(exceptionId: string, referrerId: string, value: string) {
80
+ fractionDrafts = {
81
+ ...fractionDrafts,
82
+ [exceptionId]: {
83
+ ...(fractionDrafts[exceptionId] ?? {}),
84
+ [referrerId]: value,
85
+ },
86
+ };
87
+ }
88
+
89
+ function setReason(exceptionId: string, value: string) {
90
+ reasonDrafts = { ...reasonDrafts, [exceptionId]: value };
91
+ }
92
+
93
+ function resolve(exception: AttributionExceptionView) {
94
+ const awards = draftAwards(exception);
95
+ const reason = (reasonDrafts[exception.id] ?? '').trim();
96
+ if (!validateAwards(awards).valid || reason === '') return;
97
+ onResolve?.(exception.id, awards, reason);
98
+ }
99
+ </script>
100
+
101
+ <section class="sales-attribution-queue">
102
+ {#if exceptions.length === 0}
103
+ <p class="empty">No attribution exceptions to review.</p>
104
+ {:else}
105
+ {#each exceptions as exception (exception.id)}
106
+ {@const open = exception.status === 'open'}
107
+ {@const awards = draftAwards(exception)}
108
+ {@const validation = validateAwards(awards)}
109
+ {@const reason = (reasonDrafts[exception.id] ?? '').trim()}
110
+ <Card variant="outlined">
111
+ <div class="head">
112
+ <span class="head__target">
113
+ <span class="head__kind">{exception.targetKind}</span>
114
+ {exception.targetLabel ?? exception.targetId}
115
+ </span>
116
+ <span class="head__badges">
117
+ {#if exception.programName}
118
+ <Badge variant="default" size="sm">{exception.programName}</Badge>
119
+ {/if}
120
+ <Badge variant={open ? 'warning' : 'success'} size="sm">
121
+ {exception.status}
122
+ </Badge>
123
+ </span>
124
+ </div>
125
+
126
+ <p class="conflict-reason">{exception.conflictReason}</p>
127
+
128
+ <table class="candidates">
129
+ <caption class="visually-hidden">Competing candidate touches</caption>
130
+ <thead>
131
+ <tr>
132
+ <th scope="col">Referrer</th>
133
+ <th scope="col">Touch kind</th>
134
+ <th scope="col">Occurred</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ {#each exception.candidates as candidate (candidate.touchId + candidate.referrerId)}
139
+ <tr>
140
+ <td>{candidate.referrerName ?? candidate.referrerId.slice(0, 8)}</td>
141
+ <td>{candidate.kind.replace(/_/g, ' ')}</td>
142
+ <td>{formatDate(candidate.occurredAt, locale)}</td>
143
+ </tr>
144
+ {/each}
145
+ </tbody>
146
+ </table>
147
+
148
+ {#if open && onResolve}
149
+ <div class="award-editor">
150
+ <h4>Award credit</h4>
151
+ {#each awards as award (award.referrerId)}
152
+ <div class="award-editor__row">
153
+ <span class="award-editor__name">
154
+ {referrerLabel(exception, award.referrerId)}
155
+ </span>
156
+ <Input
157
+ type="number"
158
+ min="0"
159
+ max="1"
160
+ step="0.0001"
161
+ value={fractionInputValue(exception, award.referrerId)}
162
+ disabled={busy}
163
+ aria-label={`Credit fraction for ${referrerLabel(exception, award.referrerId)}`}
164
+ oninput={(event) =>
165
+ setFraction(
166
+ exception.id,
167
+ award.referrerId,
168
+ (event.currentTarget as HTMLInputElement).value,
169
+ )}
170
+ />
171
+ </div>
172
+ {/each}
173
+ <p
174
+ class="award-editor__total"
175
+ class:invalid={!validation.valid}
176
+ role="status"
177
+ >
178
+ Total: {formatPercent(validation.totalFraction, locale)}
179
+ {#if !validation.valid && validation.message}
180
+ — {validation.message}
181
+ {/if}
182
+ </p>
183
+
184
+ <FormGroup
185
+ label="Resolution reason"
186
+ required
187
+ hint="Overrides are audited — say why this award is correct."
188
+ >
189
+ <Textarea
190
+ rows={2}
191
+ value={reasonDrafts[exception.id] ?? ''}
192
+ disabled={busy}
193
+ oninput={(event) =>
194
+ setReason(
195
+ exception.id,
196
+ (event.currentTarget as HTMLTextAreaElement).value,
197
+ )}
198
+ />
199
+ </FormGroup>
200
+
201
+ <Button
202
+ variant="primary"
203
+ size="sm"
204
+ disabled={busy || !validation.valid || reason === ''}
205
+ onclick={() => resolve(exception)}
206
+ >
207
+ Resolve
208
+ </Button>
209
+ </div>
210
+ {:else if !open}
211
+ <dl class="audit">
212
+ <div class="audit__item">
213
+ <dt>Mode</dt>
214
+ <dd>{exception.resolutionMode ?? '—'}</dd>
215
+ </div>
216
+ <div class="audit__item">
217
+ <dt>Reason</dt>
218
+ <dd>{exception.resolutionReason ?? '—'}</dd>
219
+ </div>
220
+ <div class="audit__item">
221
+ <dt>Resolved by</dt>
222
+ <dd>{exception.resolvedByName ?? '—'}</dd>
223
+ </div>
224
+ <div class="audit__item">
225
+ <dt>Resolved at</dt>
226
+ <dd>{formatDate(exception.resolvedAt, locale)}</dd>
227
+ </div>
228
+ </dl>
229
+ {/if}
230
+ </Card>
231
+ {/each}
232
+ {/if}
233
+ </section>
234
+
235
+ <style>
236
+ .sales-attribution-queue {
237
+ display: flex;
238
+ flex-direction: column;
239
+ gap: var(--smrt-spacing-4, 1rem);
240
+ }
241
+
242
+ .empty {
243
+ margin: 0;
244
+ color: var(--smrt-color-on-surface-variant, #64748b);
245
+ font-style: italic;
246
+ }
247
+
248
+ .head {
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: space-between;
252
+ gap: var(--smrt-spacing-2, 0.5rem);
253
+ margin-bottom: var(--smrt-spacing-2, 0.5rem);
254
+ }
255
+
256
+ .head__target {
257
+ font-weight: var(--smrt-typography-weight-semibold, 600);
258
+ }
259
+
260
+ .head__kind {
261
+ text-transform: uppercase;
262
+ letter-spacing: 0.04em;
263
+ font-size: var(--smrt-typography-label-medium-size, 0.75rem);
264
+ color: var(--smrt-color-on-surface-variant, #64748b);
265
+ margin-right: var(--smrt-spacing-1, 0.25rem);
266
+ }
267
+
268
+ .head__badges {
269
+ display: inline-flex;
270
+ gap: var(--smrt-spacing-1, 0.25rem);
271
+ }
272
+
273
+ .conflict-reason {
274
+ margin: 0 0 var(--smrt-spacing-3, 0.75rem);
275
+ }
276
+
277
+ .candidates {
278
+ width: 100%;
279
+ border-collapse: collapse;
280
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
281
+ margin-bottom: var(--smrt-spacing-3, 0.75rem);
282
+ }
283
+
284
+ .candidates th {
285
+ padding: var(--smrt-spacing-1, 0.25rem) var(--smrt-spacing-2, 0.5rem);
286
+ text-align: left;
287
+ font-weight: var(--smrt-typography-weight-semibold, 600);
288
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #d8dde6);
289
+ white-space: nowrap;
290
+ }
291
+
292
+ .candidates td {
293
+ padding: var(--smrt-spacing-1, 0.25rem) var(--smrt-spacing-2, 0.5rem);
294
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #d8dde6);
295
+ }
296
+
297
+ .award-editor {
298
+ display: flex;
299
+ flex-direction: column;
300
+ gap: var(--smrt-spacing-2, 0.5rem);
301
+ }
302
+
303
+ .award-editor h4 {
304
+ margin: 0;
305
+ font-size: var(--smrt-typography-label-large-size, 0.875rem);
306
+ text-transform: uppercase;
307
+ letter-spacing: 0.04em;
308
+ color: var(--smrt-color-on-surface-variant, #64748b);
309
+ }
310
+
311
+ .award-editor__row {
312
+ display: flex;
313
+ align-items: center;
314
+ gap: var(--smrt-spacing-2, 0.5rem);
315
+ }
316
+
317
+ .award-editor__name {
318
+ flex: 0 0 12rem;
319
+ }
320
+
321
+ .award-editor__total {
322
+ margin: 0;
323
+ font-size: var(--smrt-typography-body-small-size, 0.8125rem);
324
+ color: var(--smrt-color-on-surface-variant, #64748b);
325
+ }
326
+
327
+ .award-editor__total.invalid {
328
+ color: var(--smrt-color-error, #dc2626);
329
+ }
330
+
331
+ .award-editor :global(.form-group) {
332
+ margin-bottom: 0;
333
+ }
334
+
335
+ .audit {
336
+ display: flex;
337
+ flex-wrap: wrap;
338
+ gap: var(--smrt-spacing-4, 1rem);
339
+ margin: 0;
340
+ }
341
+
342
+ .audit__item dt {
343
+ color: var(--smrt-color-on-surface-variant, #64748b);
344
+ font-size: var(--smrt-typography-label-medium-size, 0.75rem);
345
+ text-transform: uppercase;
346
+ letter-spacing: 0.04em;
347
+ }
348
+
349
+ .audit__item dd {
350
+ margin: 0;
351
+ }
352
+
353
+ .visually-hidden {
354
+ position: absolute;
355
+ width: 1px;
356
+ height: 1px;
357
+ overflow: hidden;
358
+ clip: rect(0 0 0 0);
359
+ white-space: nowrap;
360
+ }
361
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { AttributionAward, AttributionExceptionView } from '../types.js';
2
+ export interface Props {
3
+ /** Exceptions to review (open and/or resolved). */
4
+ exceptions?: AttributionExceptionView[];
5
+ /** Disable actions while a resolution is in flight. */
6
+ busy?: boolean;
7
+ /** BCP 47 locale for date/percent formatting. */
8
+ locale?: string;
9
+ /** Resolve an open exception with explicit awards and a required reason. */
10
+ onResolve?: (exceptionId: string, awards: AttributionAward[], resolutionReason: string) => void;
11
+ }
12
+ declare const AttributionConflictQueue: import("svelte").Component<Props, {}, "">;
13
+ type AttributionConflictQueue = ReturnType<typeof AttributionConflictQueue>;
14
+ export default AttributionConflictQueue;
15
+ //# sourceMappingURL=AttributionConflictQueue.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttributionConflictQueue.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/AttributionConflictQueue.svelte.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAQ9E,MAAM,WAAW,KAAK;IACpB,mDAAmD;IACnD,UAAU,CAAC,EAAE,wBAAwB,EAAE,CAAC;IACxC,uDAAuD;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,CACV,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,EAAE,EAC1B,gBAAgB,EAAE,MAAM,KACrB,IAAI,CAAC;CACX;AAwLD,QAAA,MAAM,wBAAwB,2CAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
@@ -0,0 +1,251 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CommissionBreakdown — the explainable-amount surface (#1933).
4
+ *
5
+ * Table of commission rows (event, basis, base, rate, share, amount, status,
6
+ * clearing date). Each row expands to the snapshotted calculation trace —
7
+ * plan key@version, component, and the `base × rate × share = amount` formula
8
+ * line — plus the append-only adjustments recorded against it.
9
+ */
10
+ import { Badge, Button } from '@happyvertical/smrt-ui/ui';
11
+ import { formatCents, formatDate, formatPercent } from '../format.js';
12
+ import type { CommissionRowView } from '../types.js';
13
+ import {
14
+ commissionStatusBadgeVariant,
15
+ formatCommissionFormula,
16
+ formatPlanRef,
17
+ } from '../types.js';
18
+
19
+ export interface Props {
20
+ /** Commission rows for one earner (or one earning source). */
21
+ commissions?: CommissionRowView[];
22
+ /** BCP 47 locale for money/date formatting. */
23
+ locale?: string;
24
+ }
25
+
26
+ let { commissions = [], locale }: Props = $props();
27
+
28
+ const uid = $props.id();
29
+
30
+ let expanded = $state<Record<string, boolean>>({});
31
+
32
+ function toggle(id: string) {
33
+ expanded = { ...expanded, [id]: !expanded[id] };
34
+ }
35
+
36
+ function detailsId(id: string): string {
37
+ return `${uid}-commission-details-${id}`;
38
+ }
39
+ </script>
40
+
41
+ <div class="sales-commission-breakdown">
42
+ {#if commissions.length === 0}
43
+ <p class="empty">No commissions yet.</p>
44
+ {:else}
45
+ <table>
46
+ <thead>
47
+ <tr>
48
+ <th scope="col">Event</th>
49
+ <th scope="col">Basis</th>
50
+ <th scope="col" class="num">Base</th>
51
+ <th scope="col" class="num">Rate</th>
52
+ <th scope="col" class="num">Share</th>
53
+ <th scope="col" class="num">Amount</th>
54
+ <th scope="col">Status</th>
55
+ <th scope="col">Clears</th>
56
+ <th scope="col"><span class="visually-hidden">Explanation</span></th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ {#each commissions as row (row.id)}
61
+ {@const isExpanded = !!expanded[row.id]}
62
+ <tr>
63
+ <td>
64
+ <span class="event-kind">{row.eventKind.replace(/_/g, ' ')}</span>
65
+ {#if row.sourceLabel}
66
+ <span class="secondary">{row.sourceLabel}</span>
67
+ {/if}
68
+ </td>
69
+ <td>{row.basis}</td>
70
+ <td class="num">{formatCents(row.baseAmountCents, row.currency, locale)}</td>
71
+ <td class="num">
72
+ {row.basis === 'fixed' ? '—' : formatPercent(row.rate, locale)}
73
+ </td>
74
+ <td class="num">{formatPercent(row.shareFraction, locale)}</td>
75
+ <td class="num amount" class:negative={row.amountCents < 0}>
76
+ {formatCents(row.amountCents, row.currency, locale)}
77
+ </td>
78
+ <td>
79
+ <Badge variant={commissionStatusBadgeVariant(row.status)} size="sm">
80
+ {row.status}
81
+ </Badge>
82
+ </td>
83
+ <td>{formatDate(row.clearingEndsAt, locale)}</td>
84
+ <td class="toggle-cell">
85
+ <Button
86
+ variant="ghost"
87
+ size="sm"
88
+ aria-expanded={isExpanded}
89
+ aria-controls={detailsId(row.id)}
90
+ onclick={() => toggle(row.id)}
91
+ >
92
+ {isExpanded ? 'Hide' : 'Explain'}
93
+ </Button>
94
+ </td>
95
+ </tr>
96
+ {#if isExpanded}
97
+ <tr class="details-row">
98
+ <td colspan="9" id={detailsId(row.id)}>
99
+ <div class="details">
100
+ {#if row.trace}
101
+ <p class="details__ref">
102
+ Plan <code>{formatPlanRef(row.trace.planKey, row.trace.planVersion)}</code>
103
+ · component <code>{row.trace.componentKey}</code>
104
+ </p>
105
+ {/if}
106
+ <p class="details__formula">
107
+ <code>
108
+ {formatCommissionFormula(
109
+ {
110
+ basis: row.basis,
111
+ baseAmountCents: row.baseAmountCents,
112
+ rate: row.rate,
113
+ shareFraction: row.shareFraction,
114
+ amountCents: row.amountCents,
115
+ currency: row.currency,
116
+ },
117
+ locale,
118
+ )}
119
+ </code>
120
+ </p>
121
+ {#if row.adjustments && row.adjustments.length > 0}
122
+ <table class="adjustments">
123
+ <caption class="visually-hidden">
124
+ Adjustments against this commission
125
+ </caption>
126
+ <thead>
127
+ <tr>
128
+ <th scope="col">Kind</th>
129
+ <th scope="col" class="num">Amount</th>
130
+ <th scope="col">Reason</th>
131
+ <th scope="col">Recorded</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody>
135
+ {#each row.adjustments as adjustment (adjustment.id)}
136
+ <tr>
137
+ <td>{adjustment.adjustmentKind}</td>
138
+ <td class="num" class:negative={adjustment.amountCents < 0}>
139
+ {formatCents(adjustment.amountCents, adjustment.currency, locale)}
140
+ </td>
141
+ <td>{adjustment.reason}</td>
142
+ <td>{formatDate(adjustment.createdAt, locale)}</td>
143
+ </tr>
144
+ {/each}
145
+ </tbody>
146
+ </table>
147
+ {:else}
148
+ <p class="details__none">No adjustments.</p>
149
+ {/if}
150
+ </div>
151
+ </td>
152
+ </tr>
153
+ {/if}
154
+ {/each}
155
+ </tbody>
156
+ </table>
157
+ {/if}
158
+ </div>
159
+
160
+ <style>
161
+ .sales-commission-breakdown {
162
+ width: 100%;
163
+ overflow-x: auto;
164
+ }
165
+
166
+ .empty {
167
+ margin: 0;
168
+ color: var(--smrt-color-on-surface-variant, #64748b);
169
+ font-style: italic;
170
+ }
171
+
172
+ table {
173
+ width: 100%;
174
+ border-collapse: collapse;
175
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
176
+ }
177
+
178
+ th {
179
+ padding: var(--smrt-spacing-2, 0.5rem) var(--smrt-spacing-3, 0.75rem);
180
+ text-align: left;
181
+ font-weight: var(--smrt-typography-weight-semibold, 600);
182
+ background: var(--smrt-color-surface-container, #f3f4f6);
183
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #d8dde6);
184
+ white-space: nowrap;
185
+ }
186
+
187
+ td {
188
+ padding: var(--smrt-spacing-2, 0.5rem) var(--smrt-spacing-3, 0.75rem);
189
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #d8dde6);
190
+ vertical-align: top;
191
+ }
192
+
193
+ .num {
194
+ text-align: right;
195
+ white-space: nowrap;
196
+ }
197
+
198
+ .amount {
199
+ font-weight: var(--smrt-typography-weight-semibold, 600);
200
+ }
201
+
202
+ .negative {
203
+ color: var(--smrt-color-error, #dc2626);
204
+ }
205
+
206
+ .event-kind {
207
+ display: block;
208
+ font-weight: var(--smrt-typography-weight-medium, 500);
209
+ }
210
+
211
+ .secondary {
212
+ display: block;
213
+ color: var(--smrt-color-on-surface-variant, #64748b);
214
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
215
+ }
216
+
217
+ .toggle-cell {
218
+ text-align: right;
219
+ white-space: nowrap;
220
+ }
221
+
222
+ .details-row td {
223
+ background: var(--smrt-color-surface-container-low, #f9fafb);
224
+ }
225
+
226
+ .details {
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: var(--smrt-spacing-2, 0.5rem);
230
+ }
231
+
232
+ .details__ref,
233
+ .details__formula,
234
+ .details__none {
235
+ margin: 0;
236
+ }
237
+
238
+ .details__none {
239
+ color: var(--smrt-color-on-surface-variant, #64748b);
240
+ font-style: italic;
241
+ }
242
+
243
+ code {
244
+ font-family: var(--smrt-font-family-mono, ui-monospace, monospace);
245
+ font-size: var(--smrt-typography-body-small-size, 0.8125rem);
246
+ }
247
+
248
+ .adjustments th {
249
+ background: transparent;
250
+ }
251
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { CommissionRowView } from '../types.js';
2
+ export interface Props {
3
+ /** Commission rows for one earner (or one earning source). */
4
+ commissions?: CommissionRowView[];
5
+ /** BCP 47 locale for money/date formatting. */
6
+ locale?: string;
7
+ }
8
+ declare const CommissionBreakdown: import("svelte").Component<Props, {}, "">;
9
+ type CommissionBreakdown = ReturnType<typeof CommissionBreakdown>;
10
+ export default CommissionBreakdown;
11
+ //# sourceMappingURL=CommissionBreakdown.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommissionBreakdown.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/CommissionBreakdown.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAQrD,MAAM,WAAW,KAAK;IACpB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA6ID,QAAA,MAAM,mBAAmB,2CAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}