@happyvertical/smrt-commerce 0.53.4 → 0.53.6

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 (158) hide show
  1. package/AGENTS.md +19 -3
  2. package/README.md +29 -1
  3. package/agents/commercial-ui.md +78 -0
  4. package/agents/customer-vendor-ui.md +76 -0
  5. package/agents/invoice-calculations.md +17 -0
  6. package/agents/invoice-preparation.md +86 -0
  7. package/agents/invoice-quantity.md +46 -0
  8. package/agents/pricing-versions.md +28 -0
  9. package/agents/purchasing-ui.md +82 -0
  10. package/agents/quote-ui.md +82 -0
  11. package/dist/billing/units.d.ts +1 -7
  12. package/dist/billing/units.d.ts.map +1 -1
  13. package/dist/chunks/calculations-DPuldr1J.js +242 -0
  14. package/dist/chunks/calculations-DPuldr1J.js.map +1 -0
  15. package/dist/chunks/types-CRnln577.js.map +1 -1
  16. package/dist/chunks/{units-CpBepqG1.js → units-CJcAOQm1.js} +3 -39
  17. package/dist/chunks/units-CJcAOQm1.js.map +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +164 -44
  21. package/dist/index.js.map +1 -1
  22. package/dist/invoices.d.ts +3 -0
  23. package/dist/invoices.d.ts.map +1 -0
  24. package/dist/invoices.js +2 -0
  25. package/dist/manifest.json +66 -8
  26. package/dist/migrations/invoiceEditorStorage.d.ts +39 -0
  27. package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -0
  28. package/dist/models/ContractLineItem.d.ts +1 -1
  29. package/dist/models/InvoiceLineItem.d.ts +18 -36
  30. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  31. package/dist/playground.js +201 -3
  32. package/dist/playground.js.map +1 -1
  33. package/dist/smrt-knowledge.json +109 -7
  34. package/dist/svelte/components/CustomerDetail.svelte +150 -0
  35. package/dist/svelte/components/CustomerDetail.svelte.d.ts +28 -0
  36. package/dist/svelte/components/CustomerDetail.svelte.d.ts.map +1 -0
  37. package/dist/svelte/components/CustomerDirectory.svelte +177 -0
  38. package/dist/svelte/components/CustomerDirectory.svelte.d.ts +42 -0
  39. package/dist/svelte/components/CustomerDirectory.svelte.d.ts.map +1 -0
  40. package/dist/svelte/components/CustomerForm.svelte +234 -0
  41. package/dist/svelte/components/CustomerForm.svelte.d.ts +44 -0
  42. package/dist/svelte/components/CustomerForm.svelte.d.ts.map +1 -0
  43. package/dist/svelte/components/InvoiceAllocationFields.svelte +72 -0
  44. package/dist/svelte/components/InvoiceAllocationFields.svelte.d.ts +8 -0
  45. package/dist/svelte/components/InvoiceAllocationFields.svelte.d.ts.map +1 -0
  46. package/dist/svelte/components/InvoiceCard.svelte +2 -2
  47. package/dist/svelte/components/InvoiceEditor.svelte +134 -0
  48. package/dist/svelte/components/InvoiceEditor.svelte.d.ts +8 -0
  49. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -0
  50. package/dist/svelte/components/InvoiceLineEditor.svelte +85 -0
  51. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +8 -0
  52. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +1 -0
  53. package/dist/svelte/components/InvoiceReview.svelte +23 -0
  54. package/dist/svelte/components/InvoiceReview.svelte.d.ts +10 -0
  55. package/dist/svelte/components/InvoiceReview.svelte.d.ts.map +1 -0
  56. package/dist/svelte/components/PartyAddressFields.svelte +95 -0
  57. package/dist/svelte/components/PartyAddressFields.svelte.d.ts +25 -0
  58. package/dist/svelte/components/PartyAddressFields.svelte.d.ts.map +1 -0
  59. package/dist/svelte/components/PartyContactFields.svelte +153 -0
  60. package/dist/svelte/components/PartyContactFields.svelte.d.ts +28 -0
  61. package/dist/svelte/components/PartyContactFields.svelte.d.ts.map +1 -0
  62. package/dist/svelte/components/PricingVersionDecision.svelte +88 -0
  63. package/dist/svelte/components/PricingVersionDecision.svelte.d.ts +35 -0
  64. package/dist/svelte/components/PricingVersionDecision.svelte.d.ts.map +1 -0
  65. package/dist/svelte/components/PricingVersionSummary.svelte +46 -0
  66. package/dist/svelte/components/PricingVersionSummary.svelte.d.ts +9 -0
  67. package/dist/svelte/components/PricingVersionSummary.svelte.d.ts.map +1 -0
  68. package/dist/svelte/components/PurchaseOrderEditor.svelte +210 -0
  69. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +79 -0
  70. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -0
  71. package/dist/svelte/components/PurchaseSourceSelector.svelte +56 -0
  72. package/dist/svelte/components/PurchaseSourceSelector.svelte.d.ts +30 -0
  73. package/dist/svelte/components/PurchaseSourceSelector.svelte.d.ts.map +1 -0
  74. package/dist/svelte/components/PurchasingPlayground.svelte +133 -0
  75. package/dist/svelte/components/PurchasingPlayground.svelte.d.ts +9 -0
  76. package/dist/svelte/components/PurchasingPlayground.svelte.d.ts.map +1 -0
  77. package/dist/svelte/components/QuoteEditor.svelte +177 -0
  78. package/dist/svelte/components/QuoteEditor.svelte.d.ts +76 -0
  79. package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -0
  80. package/dist/svelte/components/QuotePlayground.svelte +108 -0
  81. package/dist/svelte/components/QuotePlayground.svelte.d.ts +10 -0
  82. package/dist/svelte/components/QuotePlayground.svelte.d.ts.map +1 -0
  83. package/dist/svelte/components/QuoteRevisionComparison.svelte +44 -0
  84. package/dist/svelte/components/QuoteRevisionComparison.svelte.d.ts +14 -0
  85. package/dist/svelte/components/QuoteRevisionComparison.svelte.d.ts.map +1 -0
  86. package/dist/svelte/components/QuoteRevisionHistory.svelte +55 -0
  87. package/dist/svelte/components/QuoteRevisionHistory.svelte.d.ts +15 -0
  88. package/dist/svelte/components/QuoteRevisionHistory.svelte.d.ts.map +1 -0
  89. package/dist/svelte/components/VendorDetail.svelte +134 -0
  90. package/dist/svelte/components/VendorDetail.svelte.d.ts +26 -0
  91. package/dist/svelte/components/VendorDetail.svelte.d.ts.map +1 -0
  92. package/dist/svelte/components/VendorDirectory.svelte +178 -0
  93. package/dist/svelte/components/VendorDirectory.svelte.d.ts +42 -0
  94. package/dist/svelte/components/VendorDirectory.svelte.d.ts.map +1 -0
  95. package/dist/svelte/components/VendorForm.svelte +224 -0
  96. package/dist/svelte/components/VendorForm.svelte.d.ts +42 -0
  97. package/dist/svelte/components/VendorForm.svelte.d.ts.map +1 -0
  98. package/dist/svelte/i18n.d.ts +102 -0
  99. package/dist/svelte/i18n.d.ts.map +1 -1
  100. package/dist/svelte/i18n.js +102 -0
  101. package/dist/svelte/index.d.ts +52 -0
  102. package/dist/svelte/index.d.ts.map +1 -1
  103. package/dist/svelte/index.js +49 -0
  104. package/dist/svelte/invoices/calculations.d.ts +121 -0
  105. package/dist/svelte/invoices/calculations.d.ts.map +1 -0
  106. package/dist/svelte/invoices/calculations.js +259 -0
  107. package/dist/svelte/invoices/currency.d.ts +9 -0
  108. package/dist/svelte/invoices/currency.d.ts.map +1 -0
  109. package/dist/svelte/invoices/currency.js +55 -0
  110. package/dist/svelte/invoices/fields.d.ts +6 -0
  111. package/dist/svelte/invoices/fields.d.ts.map +1 -0
  112. package/dist/svelte/invoices/fields.js +22 -0
  113. package/dist/svelte/invoices/messages.d.ts +53 -0
  114. package/dist/svelte/invoices/messages.d.ts.map +1 -0
  115. package/dist/svelte/invoices/messages.js +53 -0
  116. package/dist/svelte/invoices/types.d.ts +194 -0
  117. package/dist/svelte/invoices/types.d.ts.map +1 -0
  118. package/dist/svelte/invoices/types.js +1 -0
  119. package/dist/svelte/party-types.d.ts +203 -0
  120. package/dist/svelte/party-types.d.ts.map +1 -0
  121. package/dist/svelte/party-types.js +82 -0
  122. package/dist/svelte/playground/InvoicePreparationPreview.svelte +116 -0
  123. package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts +4 -0
  124. package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -0
  125. package/dist/svelte/playground/PartyFormPreview.svelte +211 -0
  126. package/dist/svelte/playground/PartyFormPreview.svelte.d.ts +8 -0
  127. package/dist/svelte/playground/PartyFormPreview.svelte.d.ts.map +1 -0
  128. package/dist/svelte/playground/PricingVersionPreview.svelte +55 -0
  129. package/dist/svelte/playground/PricingVersionPreview.svelte.d.ts +4 -0
  130. package/dist/svelte/playground/PricingVersionPreview.svelte.d.ts.map +1 -0
  131. package/dist/svelte/playground.d.ts +442 -3
  132. package/dist/svelte/playground.d.ts.map +1 -1
  133. package/dist/svelte/playground.js +199 -3
  134. package/dist/svelte/pricing-i18n.d.ts +10 -0
  135. package/dist/svelte/pricing-i18n.d.ts.map +1 -0
  136. package/dist/svelte/pricing-i18n.js +10 -0
  137. package/dist/svelte/pricing-types.d.ts +38 -0
  138. package/dist/svelte/pricing-types.d.ts.map +1 -0
  139. package/dist/svelte/pricing-types.js +1 -0
  140. package/dist/svelte/purchasing-i18n.d.ts +53 -0
  141. package/dist/svelte/purchasing-i18n.d.ts.map +1 -0
  142. package/dist/svelte/purchasing-i18n.js +53 -0
  143. package/dist/svelte/purchasing-types.d.ts +91 -0
  144. package/dist/svelte/purchasing-types.d.ts.map +1 -0
  145. package/dist/svelte/purchasing-types.js +13 -0
  146. package/dist/svelte/quote-i18n.d.ts +49 -0
  147. package/dist/svelte/quote-i18n.d.ts.map +1 -0
  148. package/dist/svelte/quote-i18n.js +49 -0
  149. package/dist/svelte/quote-types.d.ts +81 -0
  150. package/dist/svelte/quote-types.d.ts.map +1 -0
  151. package/dist/svelte/quote-types.js +51 -0
  152. package/dist/types/index.d.ts +13 -3
  153. package/dist/types/index.d.ts.map +1 -1
  154. package/dist/ui.d.ts.map +1 -1
  155. package/dist/ui.js +150 -6
  156. package/dist/ui.js.map +1 -1
  157. package/package.json +25 -13
  158. package/dist/chunks/units-CpBepqG1.js.map +0 -1
@@ -0,0 +1,177 @@
1
+ <script lang="ts">
2
+ import { StatusBadge } from '@happyvertical/smrt-ui';
3
+ import { Alert } from '@happyvertical/smrt-ui/feedback';
4
+ import { Form, FormGroup, Input, Select } from '@happyvertical/smrt-ui/forms';
5
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
6
+ import { EmptyState, PageHeader } from '@happyvertical/smrt-ui/layout';
7
+ import { Button, Card, Skeleton } from '@happyvertical/smrt-ui/ui';
8
+ import { M } from '../i18n.js';
9
+ import type {
10
+ CustomerDisplayData,
11
+ PartyDirectoryExtension,
12
+ PartyDirectoryItem,
13
+ PartySurfaceLabels,
14
+ } from '../party-types.js';
15
+
16
+ /** Props for the searchable, paginated customer directory. */
17
+ export interface Props {
18
+ /** Customer rows and caller-owned detail links. */
19
+ items?: PartyDirectoryItem<CustomerDisplayData>[];
20
+ /** Total matching rows across every page. */
21
+ total?: number;
22
+ /** Current one-based page number. */
23
+ page?: number;
24
+ /** Number of rows requested per page. */
25
+ pageSize?: number;
26
+ /** Search text reflected from the caller's query state. */
27
+ query?: string;
28
+ /** Status filter reflected from the caller's query state. */
29
+ status?: string;
30
+ /** Caller-owned URL for the native search form. */
31
+ searchAction?: string;
32
+ /** HTML name used for the search query field. */
33
+ queryName?: string;
34
+ /** HTML name used for the status field. */
35
+ statusName?: string;
36
+ /** Caller-owned URL for the previous page. */
37
+ previousHref?: string;
38
+ /** Caller-owned URL for the next page. */
39
+ nextHref?: string;
40
+ /** Caller-owned URL for creating a customer. */
41
+ addHref?: string;
42
+ /** Controls whether the create affordance is presented. */
43
+ canCreate?: boolean;
44
+ /** Renders loading placeholders instead of current rows. */
45
+ loading?: boolean;
46
+ /** Server or upstream failure to present instead of current rows. */
47
+ errorMessage?: string;
48
+ /** Customer terminology and action-label overrides. */
49
+ labels?: PartySurfaceLabels;
50
+ /** Consumer content appended to every customer row. */
51
+ extension?: PartyDirectoryExtension<CustomerDisplayData>;
52
+ }
53
+
54
+ const {
55
+ items = [],
56
+ total = items.length,
57
+ page = 1,
58
+ pageSize,
59
+ query = '',
60
+ status = '',
61
+ searchAction,
62
+ queryName = 'q',
63
+ statusName = 'status',
64
+ previousHref,
65
+ nextHref,
66
+ addHref,
67
+ canCreate = false,
68
+ loading = false,
69
+ errorMessage,
70
+ labels = {},
71
+ extension,
72
+ }: Props = $props();
73
+
74
+ const { t } = useI18n();
75
+ const singular = $derived(
76
+ labels.singular ?? t(M['commerce.customer.singular']),
77
+ );
78
+ const plural = $derived(labels.plural ?? t(M['commerce.customer.plural']));
79
+ const first = $derived(
80
+ pageSize === undefined || total === 0 ? 0 : (page - 1) * pageSize + 1,
81
+ );
82
+ const last = $derived(
83
+ pageSize === undefined ? 0 : Math.min(page * pageSize, total),
84
+ );
85
+
86
+ function statusLabel(value: string): string {
87
+ if (value === 'active') return t(M['commerce.party.status_active']);
88
+ if (value === 'inactive') return t(M['commerce.party.status_inactive']);
89
+ if (value === 'suspended') return t(M['commerce.party.status_suspended']);
90
+ return value;
91
+ }
92
+
93
+ function customerTypeLabel(value: string | undefined): string {
94
+ if (value === 'dtc') return t(M['commerce.customer.type_dtc']);
95
+ if (value === 'wholesale') return t(M['commerce.customer.type_wholesale']);
96
+ if (value === 'retail') return t(M['commerce.customer.type_retail']);
97
+ return value ?? singular;
98
+ }
99
+ </script>
100
+
101
+ <section class="party-directory">
102
+ <PageHeader title={plural} subtitle={labels.plural ? t(M['commerce.party.manage_named'], { plural }) : t(M['commerce.customer.manage'])}>
103
+ {#snippet actions()}
104
+ {#if canCreate && addHref}<Button href={addHref}>{labels.add ?? (labels.singular ? t(M['commerce.party.add_named'], { singular }) : t(M['commerce.customer.add']))}</Button>{/if}
105
+ {/snippet}
106
+ </PageHeader>
107
+
108
+ <Form class="directory-search" method="get" action={searchAction} preventDefault={false} stagedReview={false}>
109
+ <FormGroup label={labels.search ?? (labels.plural ? t(M['commerce.party.search_named'], { plural }) : t(M['commerce.customer.search']))}>
110
+ <Input type="search" name={queryName} value={query} maxlength={160} />
111
+ </FormGroup>
112
+ <FormGroup label={t(M['commerce.party.status'])}>
113
+ <Select name={statusName} value={status}>
114
+ <option value="">{t(M['commerce.party.status_all'])}</option>
115
+ {#if status && !['active', 'inactive', 'suspended'].includes(status)}
116
+ <option value={status}>{status}</option>
117
+ {/if}
118
+ <option value="active">{t(M['commerce.party.status_active'])}</option>
119
+ <option value="inactive">{t(M['commerce.party.status_inactive'])}</option>
120
+ <option value="suspended">{t(M['commerce.party.status_suspended'])}</option>
121
+ </Select>
122
+ </FormGroup>
123
+ <Button type="submit" variant="secondary">{t(M['commerce.party.search'])}</Button>
124
+ </Form>
125
+
126
+ {#if errorMessage}
127
+ <Alert variant="error" title={t(M['commerce.customer.unable_to_load'])}>{errorMessage}</Alert>
128
+ {:else if loading}
129
+ <div class="loading" role="status" aria-label={labels.loading ?? t(M['commerce.customer.loading'])}>
130
+ <Skeleton height="6rem" /><Skeleton height="6rem" /><Skeleton height="6rem" />
131
+ </div>
132
+ {:else if items.length === 0}
133
+ <EmptyState title={labels.empty ?? t(M['commerce.customer.no_results'])} description={t(M['commerce.party.try_filters'])} />
134
+ {:else}
135
+ <p class="result-count">
136
+ {pageSize === undefined
137
+ ? t(M['commerce.party.showing_count'], { count: items.length, total })
138
+ : t(M['commerce.party.showing'], { first, last, total })}
139
+ </p>
140
+ <div class="party-list">
141
+ {#each items as item (item.data.id)}
142
+ <Card padding="md" hoverable data-party-id={item.data.id}>
143
+ <div class="party-row">
144
+ <div class="party-copy">
145
+ <h2>{#if item.href}<a href={item.href}>{item.data.profile.name}</a>{:else}{item.data.profile.name}{/if}</h2>
146
+ <p>{item.data.profile.email || t(M['commerce.party.no_contact_details'])}</p>
147
+ <p class="meta">{customerTypeLabel(item.data.customerType)}{item.data.paymentTerms ? ` · ${item.data.paymentTerms}` : ''}</p>
148
+ </div>
149
+ <StatusBadge status={item.data.status} label={statusLabel(item.data.status)} />
150
+ </div>
151
+ {@render extension?.(item.data)}
152
+ </Card>
153
+ {/each}
154
+ </div>
155
+ <nav class="pagination" aria-label={t(M['commerce.party.pagination'], { singular })}>
156
+ {#if previousHref}<Button href={previousHref} variant="secondary">{labels.previous ?? t(M['commerce.party.previous'])}</Button>{/if}
157
+ <span>{t(M['commerce.party.page'], { page })}</span>
158
+ {#if nextHref}<Button href={nextHref} variant="secondary">{labels.next ?? t(M['commerce.party.next'])}</Button>{/if}
159
+ </nav>
160
+ {/if}
161
+ </section>
162
+
163
+ <style>
164
+ .party-directory { display: grid; gap: var(--smrt-spacing-5); max-inline-size: 64rem; min-inline-size: 0; }
165
+ :global(.directory-search) { display: grid; grid-template-columns: minmax(12rem, 2fr) minmax(10rem, 1fr) auto; align-items: end; gap: var(--smrt-spacing-3); }
166
+ :global(.directory-search .form-group) { margin: 0; min-inline-size: 0; }
167
+ .loading, .party-list { display: grid; gap: var(--smrt-spacing-3); }
168
+ .result-count { margin: 0; color: var(--smrt-color-on-surface-variant); }
169
+ .party-row { display: flex; align-items: start; justify-content: space-between; gap: var(--smrt-spacing-4); }
170
+ .party-copy { min-inline-size: 0; }
171
+ h2, p { margin: 0; }
172
+ h2 { font: var(--smrt-typography-title-medium-font); }
173
+ h2 a { color: var(--smrt-color-primary); text-decoration-thickness: 1px; text-underline-offset: 0.15em; }
174
+ .meta { margin-block-start: var(--smrt-spacing-2); color: var(--smrt-color-on-surface-variant); }
175
+ .pagination { display: flex; align-items: center; justify-content: center; gap: var(--smrt-spacing-3); }
176
+ @media (max-width: 42rem) { :global(.directory-search) { grid-template-columns: minmax(0, 1fr); } :global(.directory-search .button) { inline-size: 100%; } .party-row { align-items: start; } }
177
+ </style>
@@ -0,0 +1,42 @@
1
+ import type { CustomerDisplayData, PartyDirectoryExtension, PartyDirectoryItem, PartySurfaceLabels } from '../party-types.js';
2
+ /** Props for the searchable, paginated customer directory. */
3
+ export interface Props {
4
+ /** Customer rows and caller-owned detail links. */
5
+ items?: PartyDirectoryItem<CustomerDisplayData>[];
6
+ /** Total matching rows across every page. */
7
+ total?: number;
8
+ /** Current one-based page number. */
9
+ page?: number;
10
+ /** Number of rows requested per page. */
11
+ pageSize?: number;
12
+ /** Search text reflected from the caller's query state. */
13
+ query?: string;
14
+ /** Status filter reflected from the caller's query state. */
15
+ status?: string;
16
+ /** Caller-owned URL for the native search form. */
17
+ searchAction?: string;
18
+ /** HTML name used for the search query field. */
19
+ queryName?: string;
20
+ /** HTML name used for the status field. */
21
+ statusName?: string;
22
+ /** Caller-owned URL for the previous page. */
23
+ previousHref?: string;
24
+ /** Caller-owned URL for the next page. */
25
+ nextHref?: string;
26
+ /** Caller-owned URL for creating a customer. */
27
+ addHref?: string;
28
+ /** Controls whether the create affordance is presented. */
29
+ canCreate?: boolean;
30
+ /** Renders loading placeholders instead of current rows. */
31
+ loading?: boolean;
32
+ /** Server or upstream failure to present instead of current rows. */
33
+ errorMessage?: string;
34
+ /** Customer terminology and action-label overrides. */
35
+ labels?: PartySurfaceLabels;
36
+ /** Consumer content appended to every customer row. */
37
+ extension?: PartyDirectoryExtension<CustomerDisplayData>;
38
+ }
39
+ declare const CustomerDirectory: import("svelte").Component<Props, {}, "">;
40
+ type CustomerDirectory = ReturnType<typeof CustomerDirectory>;
41
+ export default CustomerDirectory;
42
+ //# sourceMappingURL=CustomerDirectory.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomerDirectory.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/CustomerDirectory.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAG3B,8DAA8D;AAC9D,MAAM,WAAW,KAAK;IACpB,mDAAmD;IACnD,KAAK,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,EAAE,CAAC;IAClD,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,uDAAuD;IACvD,SAAS,CAAC,EAAE,uBAAuB,CAAC,mBAAmB,CAAC,CAAC;CAC1D;AA6HD,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,234 @@
1
+ <script lang="ts">
2
+ import type { CodeSelectOption } from '@happyvertical/smrt-ui/forms';
3
+ import {
4
+ Checkbox,
5
+ ErrorSummary,
6
+ Form,
7
+ FormActionBar,
8
+ FormGroup,
9
+ Input,
10
+ Select,
11
+ Textarea,
12
+ } from '@happyvertical/smrt-ui/forms';
13
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
14
+ import { Button } from '@happyvertical/smrt-ui/ui';
15
+ import type { Snippet } from 'svelte';
16
+ import { M } from '../i18n.js';
17
+ import type {
18
+ CustomerFieldNames,
19
+ CustomerFormValues,
20
+ PartyContactLabels,
21
+ PartyExtension,
22
+ PartyFormErrors,
23
+ PartyFormTransport,
24
+ PartySurfaceLabels,
25
+ } from '../party-types.js';
26
+ import { DEFAULT_CUSTOMER_FIELD_NAMES } from '../party-types.js';
27
+ import PartyAddressFields from './PartyAddressFields.svelte';
28
+ import PartyContactFields from './PartyContactFields.svelte';
29
+
30
+ /** Props for the native, SSR-safe customer create/edit form. */
31
+ export interface Props {
32
+ /** Selects create or edit copy and identity-lock defaults. */
33
+ mode?: 'create' | 'edit';
34
+ /** Exact customer values to render, including server-retained strings. */
35
+ values?: CustomerFormValues;
36
+ /** Caller-owned native form action, method, tokens, and submitter intents. */
37
+ transport?: PartyFormTransport;
38
+ /** HTML field-name overrides used to preserve an existing payload contract. */
39
+ fieldNames?: Partial<CustomerFieldNames>;
40
+ /** Form, field, and contact errors returned by the server. */
41
+ errors?: PartyFormErrors;
42
+ /** Customer terminology and action-label overrides. */
43
+ labels?: PartySurfaceLabels;
44
+ /** Contact terminology overrides. */
45
+ contactLabels?: PartyContactLabels;
46
+ /** Caller-restricted country choices for both addresses. */
47
+ countryOptions?: readonly CodeSelectOption[];
48
+ /** Caller-restricted region choices for both addresses. */
49
+ provinceOptions?: readonly CodeSelectOption[];
50
+ /** Renders values without mutation affordances. */
51
+ readOnly?: boolean;
52
+ /** Controls whether the save submitter is presented. */
53
+ canSubmit?: boolean;
54
+ /** Controls whether the native add-contact submitter is presented. */
55
+ canAddContact?: boolean;
56
+ /** Controls whether native remove-contact submitters are presented. */
57
+ canRemoveContact?: boolean;
58
+ /** Keeps an established Profile identity category from being converted. */
59
+ lockIdentityKind?: boolean;
60
+ /** Caller-owned URL used by the default cancel action. */
61
+ cancelHref?: string;
62
+ /** Consumer fields rendered before the action bar. */
63
+ extension?: PartyExtension<CustomerFormValues>;
64
+ /** Replaces the default cancel and save action bar. */
65
+ actions?: Snippet;
66
+ }
67
+
68
+ const {
69
+ mode = 'create',
70
+ values = {},
71
+ transport = {},
72
+ fieldNames = {},
73
+ errors = {},
74
+ labels = {},
75
+ contactLabels,
76
+ countryOptions,
77
+ provinceOptions,
78
+ readOnly = false,
79
+ canSubmit = true,
80
+ canAddContact = true,
81
+ canRemoveContact = true,
82
+ lockIdentityKind = mode === 'edit',
83
+ cancelHref,
84
+ extension,
85
+ actions,
86
+ }: Props = $props();
87
+
88
+ const { t } = useI18n();
89
+ const singular = $derived(
90
+ labels.singular ?? t(M['commerce.customer.singular']),
91
+ );
92
+ const names = $derived({ ...DEFAULT_CUSTOMER_FIELD_NAMES, ...fieldNames });
93
+ const intentName = $derived(transport.intentName ?? 'intent');
94
+ const errorList = $derived(
95
+ Object.entries(errors.fields ?? {})
96
+ .filter((entry): entry is [string, string] => typeof entry[1] === 'string')
97
+ .map(([field, message]) => ({
98
+ controlId:
99
+ field in names ? names[field as keyof CustomerFieldNames] : field,
100
+ message,
101
+ })),
102
+ );
103
+ </script>
104
+
105
+ <Form
106
+ class="party-form"
107
+ action={transport.action}
108
+ method={transport.method ?? 'post'}
109
+ preventDefault={false}
110
+ stagedReview={false}
111
+ >
112
+ {#each Object.entries(transport.hiddenFields ?? {}) as [name, value]}
113
+ <Input interaction={false} type="hidden" {name} {value} />
114
+ {/each}
115
+ {#if values.profileId}
116
+ <Input interaction={false} type="hidden" name={names.profileId} value={values.profileId} />
117
+ {/if}
118
+ {#if !readOnly && canSubmit}
119
+ <span class="native-default-submit" aria-hidden="true">
120
+ <Button type="submit" name={intentName} value={transport.saveIntent ?? 'save'} tabindex={-1}>
121
+ {labels.save ?? (labels.singular ? t(M[mode === 'create' ? 'commerce.party.add_named' : 'commerce.party.save_named'], { singular: labels.singular }) : t(M[mode === 'create' ? 'commerce.customer.add' : 'commerce.customer.save']))}
122
+ </Button>
123
+ </span>
124
+ {/if}
125
+
126
+ {#if errors.form}<p class="form-error" role="alert">{errors.form}</p>{/if}
127
+ <ErrorSummary errors={errorList} />
128
+
129
+ <fieldset class="form-section">
130
+ <legend>{t(M['commerce.party.identity'])}</legend>
131
+ {#if lockIdentityKind && values.identityKind}
132
+ <p class="locked-value">{t(M['commerce.party.identity'])}: {values.identityKind === 'business' ? t(M['commerce.party.identity_business']) : t(M['commerce.party.identity_person'])}</p>
133
+ <Input interaction={false} type="hidden" name={names.identityKind} value={values.identityKind} />
134
+ {:else}
135
+ <FormGroup label={t(M['commerce.party.identity'])} required error={errors.fields?.identityKind}>
136
+ <Select name={names.identityKind} value={values.identityKind ?? ''} required disabled={readOnly}>
137
+ <option value="">{t(M['commerce.party.select_identity'])}</option>
138
+ <option value="business">{t(M['commerce.party.identity_business'])}</option>
139
+ <option value="person">{t(M['commerce.party.identity_person'])}</option>
140
+ </Select>
141
+ </FormGroup>
142
+ {/if}
143
+ <FormGroup label={t(M['commerce.party.name'], { singular })} required error={errors.fields?.name}>
144
+ <Input name={names.name} value={values.name ?? ''} maxlength={160} required readonly={readOnly} />
145
+ </FormGroup>
146
+ <div class="field-grid">
147
+ <FormGroup label={t(M['commerce.party.profile_email'])} error={errors.fields?.email}>
148
+ <Input name={names.email} type="email" value={values.email ?? ''} maxlength={320} readonly={readOnly} />
149
+ </FormGroup>
150
+ <FormGroup label={t(M['commerce.customer.type'])} error={errors.fields?.customerType}>
151
+ <Select name={names.customerType} value={values.customerType ?? 'dtc'} disabled={readOnly}>
152
+ {#if values.customerType && !['dtc', 'wholesale', 'retail'].includes(values.customerType)}
153
+ <option value={values.customerType}>{values.customerType}</option>
154
+ {/if}
155
+ <option value="dtc">{t(M['commerce.customer.type_dtc'])}</option>
156
+ <option value="wholesale">{t(M['commerce.customer.type_wholesale'])}</option>
157
+ <option value="retail">{t(M['commerce.customer.type_retail'])}</option>
158
+ </Select>
159
+ </FormGroup>
160
+ </div>
161
+ <FormGroup label={t(M['commerce.party.description'])} error={errors.fields?.description}>
162
+ <Textarea name={names.description} value={values.description ?? ''} rows={3} maxlength={1000} readonly={readOnly} />
163
+ </FormGroup>
164
+ </fieldset>
165
+
166
+ <fieldset class="form-section">
167
+ <legend>{t(M['commerce.party.commercial_terms'])}</legend>
168
+ <div class="field-grid">
169
+ <FormGroup label={t(M['commerce.party.status'])} error={errors.fields?.status}>
170
+ <Select name={names.status} value={values.status ?? 'active'} disabled={readOnly}>
171
+ {#if values.status && !['active', 'inactive', 'suspended'].includes(values.status)}
172
+ <option value={values.status}>{values.status}</option>
173
+ {/if}
174
+ <option value="active">{t(M['commerce.party.status_active'])}</option>
175
+ <option value="inactive">{t(M['commerce.party.status_inactive'])}</option>
176
+ <option value="suspended">{t(M['commerce.party.status_suspended'])}</option>
177
+ </Select>
178
+ </FormGroup>
179
+ <FormGroup label={t(M['commerce.customer.credit_limit'])} hint={t(M['commerce.customer.credit_limit_hint'])} error={errors.fields?.creditLimit}>
180
+ <Input name={names.creditLimit} inputmode="decimal" value={values.creditLimit ?? ''} readonly={readOnly} />
181
+ </FormGroup>
182
+ <FormGroup label={t(M['commerce.party.payment_terms'])} error={errors.fields?.paymentTerms}>
183
+ <Input name={names.paymentTerms} value={values.paymentTerms ?? ''} maxlength={120} readonly={readOnly} />
184
+ </FormGroup>
185
+ </div>
186
+ <Checkbox name={names.taxExempt} value="true" checked={values.taxExempt ?? false} disabled={readOnly} label={t(M['commerce.customer.tax_exempt'])} />
187
+ <FormGroup label={t(M['commerce.party.internal_notes'])} error={errors.fields?.notes}>
188
+ <Textarea name={names.notes} value={values.notes ?? ''} rows={4} maxlength={4000} readonly={readOnly} />
189
+ </FormGroup>
190
+ </fieldset>
191
+
192
+ <div class="address-pair">
193
+ <PartyAddressFields heading={t(M['commerce.party.shipping_address'])} value={values.shippingAddress} names={{ street1: names.shippingStreet1, street2: names.shippingStreet2, city: names.shippingCity, state: names.shippingState, postalCode: names.shippingPostalCode, country: names.shippingCountry }} errors={{ street1: errors.fields?.shippingStreet1, street2: errors.fields?.shippingStreet2, city: errors.fields?.shippingCity, state: errors.fields?.shippingState, postalCode: errors.fields?.shippingPostalCode, country: errors.fields?.shippingCountry }} {readOnly} {countryOptions} {provinceOptions} />
194
+ <PartyAddressFields heading={t(M['commerce.party.billing_address'])} value={values.billingAddress} names={{ street1: names.billingStreet1, street2: names.billingStreet2, city: names.billingCity, state: names.billingState, postalCode: names.billingPostalCode, country: names.billingCountry }} errors={{ street1: errors.fields?.billingStreet1, street2: errors.fields?.billingStreet2, city: errors.fields?.billingCity, state: errors.fields?.billingState, postalCode: errors.fields?.billingPostalCode, country: errors.fields?.billingCountry }} {readOnly} {countryOptions} {provinceOptions} />
195
+ </div>
196
+
197
+ <PartyContactFields
198
+ contacts={values.contacts ?? []}
199
+ fieldNames={names}
200
+ errors={errors.contacts}
201
+ labels={contactLabels}
202
+ readOnly={readOnly}
203
+ canAdd={canAddContact}
204
+ canRemove={canRemoveContact}
205
+ {intentName}
206
+ addIntent={transport.addContactIntent}
207
+ removeIntent={transport.removeContactIntent}
208
+ />
209
+
210
+ {@render extension?.(values)}
211
+
212
+ {#if actions}
213
+ {@render actions()}
214
+ {:else if !readOnly && canSubmit}
215
+ <FormActionBar label={t(M['commerce.party.form_actions'], { singular })}>
216
+ {#if cancelHref}<Button href={cancelHref} variant="ghost">{labels.cancel ?? t(M['commerce.party.cancel'])}</Button>{/if}
217
+ <Button type="submit" name={intentName} value={transport.saveIntent ?? 'save'}>
218
+ {labels.save ?? (labels.singular ? t(M[mode === 'create' ? 'commerce.party.add_named' : 'commerce.party.save_named'], { singular: labels.singular }) : t(M[mode === 'create' ? 'commerce.customer.add' : 'commerce.customer.save']))}
219
+ </Button>
220
+ </FormActionBar>
221
+ {/if}
222
+ </Form>
223
+
224
+ <style>
225
+ :global(.party-form) { display: grid; gap: var(--smrt-spacing-5); min-inline-size: 0; padding-block-end: var(--smrt-spacing-8); }
226
+ .native-default-submit { position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
227
+ .form-error { margin: 0; padding: var(--smrt-spacing-4); border-inline-start: 4px solid var(--smrt-color-error); background: var(--smrt-color-error-container); color: var(--smrt-color-on-error-container); border-radius: var(--smrt-radius-small); }
228
+ .form-section { min-inline-size: 0; margin: 0; padding: var(--smrt-spacing-4); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-medium); }
229
+ legend { padding-inline: var(--smrt-spacing-2); font: var(--smrt-typography-title-medium-font); color: var(--smrt-color-on-surface); }
230
+ .locked-value { color: var(--smrt-color-on-surface-variant); }
231
+ .field-grid, .address-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--smrt-spacing-4); }
232
+ .address-pair { gap: var(--smrt-spacing-4); }
233
+ @media (max-width: 42rem) { .field-grid, .address-pair { grid-template-columns: minmax(0, 1fr); } }
234
+ </style>
@@ -0,0 +1,44 @@
1
+ import type { CodeSelectOption } from '@happyvertical/smrt-ui/forms';
2
+ import type { Snippet } from 'svelte';
3
+ import type { CustomerFieldNames, CustomerFormValues, PartyContactLabels, PartyExtension, PartyFormErrors, PartyFormTransport, PartySurfaceLabels } from '../party-types.js';
4
+ /** Props for the native, SSR-safe customer create/edit form. */
5
+ export interface Props {
6
+ /** Selects create or edit copy and identity-lock defaults. */
7
+ mode?: 'create' | 'edit';
8
+ /** Exact customer values to render, including server-retained strings. */
9
+ values?: CustomerFormValues;
10
+ /** Caller-owned native form action, method, tokens, and submitter intents. */
11
+ transport?: PartyFormTransport;
12
+ /** HTML field-name overrides used to preserve an existing payload contract. */
13
+ fieldNames?: Partial<CustomerFieldNames>;
14
+ /** Form, field, and contact errors returned by the server. */
15
+ errors?: PartyFormErrors;
16
+ /** Customer terminology and action-label overrides. */
17
+ labels?: PartySurfaceLabels;
18
+ /** Contact terminology overrides. */
19
+ contactLabels?: PartyContactLabels;
20
+ /** Caller-restricted country choices for both addresses. */
21
+ countryOptions?: readonly CodeSelectOption[];
22
+ /** Caller-restricted region choices for both addresses. */
23
+ provinceOptions?: readonly CodeSelectOption[];
24
+ /** Renders values without mutation affordances. */
25
+ readOnly?: boolean;
26
+ /** Controls whether the save submitter is presented. */
27
+ canSubmit?: boolean;
28
+ /** Controls whether the native add-contact submitter is presented. */
29
+ canAddContact?: boolean;
30
+ /** Controls whether native remove-contact submitters are presented. */
31
+ canRemoveContact?: boolean;
32
+ /** Keeps an established Profile identity category from being converted. */
33
+ lockIdentityKind?: boolean;
34
+ /** Caller-owned URL used by the default cancel action. */
35
+ cancelHref?: string;
36
+ /** Consumer fields rendered before the action bar. */
37
+ extension?: PartyExtension<CustomerFormValues>;
38
+ /** Replaces the default cancel and save action bar. */
39
+ actions?: Snippet;
40
+ }
41
+ declare const CustomerForm: import("svelte").Component<Props, {}, "">;
42
+ type CustomerForm = ReturnType<typeof CustomerForm>;
43
+ export default CustomerForm;
44
+ //# sourceMappingURL=CustomerForm.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomerForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/CustomerForm.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAarE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAM3B,gEAAgE;AAChE,MAAM,WAAW,KAAK;IACpB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACzB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,8EAA8E;IAC9E,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACzC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,uDAAuD;IACvD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,qCAAqC;IACrC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,4DAA4D;IAC5D,cAAc,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,2DAA2D;IAC3D,eAAe,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sEAAsE;IACtE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,SAAS,CAAC,EAAE,cAAc,CAAC,kBAAkB,CAAC,CAAC;IAC/C,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA6JD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import {
3
+ Checkbox,
4
+ FormGroup,
5
+ Input,
6
+ Select,
7
+ } from '@happyvertical/smrt-ui/forms';
8
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
9
+ import { Button } from '@happyvertical/smrt-ui/ui';
10
+ import { invoicePreparationMessages as M } from '../invoices/messages.js';
11
+ import type { InvoiceAllocationFieldsProps } from '../invoices/types.js';
12
+ /** Optional allocation fields; compose inside a caller-owned invoice form. */
13
+ export interface Props extends InvoiceAllocationFieldsProps {}
14
+ let {
15
+ currency,
16
+ allocations,
17
+ sources = [],
18
+ mayReadSources = true,
19
+ canEdit = true,
20
+ pending = false,
21
+ fields = {},
22
+ addIntent = 'addAllocation',
23
+ id,
24
+ }: Props = $props();
25
+ const { t } = useI18n();
26
+ const instanceId = $props.id();
27
+ const formId = $derived(id ?? `invoice-allocations-${instanceId}`);
28
+ const names = $derived({
29
+ source: 'sourceId',
30
+ amount: 'allocationAmount',
31
+ remove: 'removeRow',
32
+ intent: 'intent',
33
+ ...fields,
34
+ });
35
+ </script>
36
+ <div class="allocations">
37
+ {#if !mayReadSources}<p>{t(M['commerce.preparation.restricted'])}</p>
38
+ {:else if sources.length === 0}<p>{t(M['commerce.preparation.empty'])}</p>{/if}
39
+ {#if allocations.length === 0}<p>{t(M['commerce.preparation.no_rows'])}</p>{/if}
40
+ {#each allocations as allocation, index (allocation.key)}
41
+ <fieldset disabled={!canEdit}>
42
+ <legend>{t(M['commerce.preparation.allocation'], { number: index + 1 })}</legend>
43
+ <FormGroup label={t(M[mayReadSources ? 'commerce.preparation.source' : 'commerce.preparation.reference'])} id={`${formId}-source-${index}`} error={allocation.sourceError}>
44
+ {#if mayReadSources}
45
+ <Select name={names.source} value={allocation.sourceId} interaction={{ id: `source-${allocation.key}` }}>
46
+ <option value="">{t(M['commerce.preparation.choose'])}</option>
47
+ {#if allocation.sourceId && !sources.some(source => source.id === allocation.sourceId)}
48
+ <option value={allocation.sourceId}>{t(M['commerce.preparation.retained'], { reference: allocation.sourceId })}</option>
49
+ {/if}
50
+ {#each sources as source}
51
+ <option value={source.id} disabled={source.selectable === false && source.id !== allocation.sourceId}>{t(M['commerce.preparation.available'], { label: source.label, amount: source.availableMinor, currency })}</option>
52
+ {/each}
53
+ </Select>
54
+ {:else}
55
+ <Input name={names.source} value={allocation.sourceId} interaction={{ id: `source-${allocation.key}` }} />
56
+ {/if}
57
+ </FormGroup>
58
+ <FormGroup label={t(M['commerce.preparation.amount'], { currency })} id={`${formId}-amount-${index}`} error={allocation.amountError}>
59
+ <Input name={names.amount} value={allocation.amount} inputmode="decimal" aria-describedby={allocation.amountError ? `${formId}-amount-${index}-error` : `${formId}-help`} interaction={{ id: `amount-${allocation.key}` }} />
60
+ </FormGroup>
61
+ {#if canEdit}<Checkbox name={names.remove} value={String(index)} checked={allocation.remove ?? false} label={t(M['commerce.preparation.remove'])} interaction={{ id: `remove-${allocation.key}` }} />{/if}
62
+ </fieldset>
63
+ {/each}
64
+
65
+ <p id={`${formId}-help`}>{t(M['commerce.preparation.help'])}</p>
66
+ {#if canEdit}<Button type="submit" name={names.intent} value={addIntent} formnovalidate disabled={pending} variant="secondary">{t(M['commerce.preparation.add'])}</Button>{/if}
67
+ </div>
68
+ <style>
69
+ .allocations { display: grid; gap: var(--smrt-spacing-4); min-width: 0; }
70
+ fieldset { min-width: 0; margin: 0; padding: var(--smrt-spacing-4); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); }
71
+ p { margin: 0; }
72
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { InvoiceAllocationFieldsProps } from '../invoices/types.js';
2
+ /** Optional allocation fields; compose inside a caller-owned invoice form. */
3
+ export interface Props extends InvoiceAllocationFieldsProps {
4
+ }
5
+ declare const InvoiceAllocationFields: import("svelte").Component<Props, {}, "">;
6
+ type InvoiceAllocationFields = ReturnType<typeof InvoiceAllocationFields>;
7
+ export default InvoiceAllocationFields;
8
+ //# sourceMappingURL=InvoiceAllocationFields.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvoiceAllocationFields.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InvoiceAllocationFields.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AAEzE,8EAA8E;AAC9E,MAAM,WAAW,KAAM,SAAQ,4BAA4B;CAAG;AAkE9D,QAAA,MAAM,uBAAuB,2CAAwC,CAAC;AACtE,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAC1E,eAAe,uBAAuB,CAAC"}
@@ -23,8 +23,8 @@ export interface Props {
23
23
 
24
24
  const { invoice, currency = 'CAD', href, onclick }: Props = $props();
25
25
 
26
- // Format money. Amounts are decimal dollars (the commerce models store DECIMAL
27
- // dollars — AGENTS.md "Currency in decimal fields"), not integer cents.
26
+ // This legacy InvoiceData display contract uses major-unit amounts.
27
+ // Convert integer minor-unit model values in the caller adapter.
28
28
  function formatMoney(amount: number): string {
29
29
  return new Intl.NumberFormat('en-CA', {
30
30
  style: 'currency',