@happyvertical/smrt-commerce 0.53.4 → 0.53.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,133 @@
1
+ <script lang="ts">
2
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
+ import { Button } from '@happyvertical/smrt-ui/ui';
4
+ import { P } from '../purchasing-i18n.js';
5
+ import type {
6
+ PurchaseDraftValues,
7
+ PurchaseReviewSnapshot,
8
+ } from '../purchasing-types.js';
9
+ import PurchaseOrderEditor from './PurchaseOrderEditor.svelte';
10
+ import PurchaseSourceSelector from './PurchaseSourceSelector.svelte';
11
+ /** Interactive purchasing form demo; no record is written. */
12
+ export interface Props {
13
+ /** Show the explicit retained-allocation reduction workflow. */
14
+ reduction?: boolean;
15
+ }
16
+ let { reduction = false }: Props = $props();
17
+ const { t } = useI18n();
18
+ let values = $state<PurchaseDraftValues>({
19
+ instrument: 'purchase-order',
20
+ tax: '0.00',
21
+ scope: 'Deliver design services',
22
+ inclusions: 'Two review rounds',
23
+ exclusions: 'Printing',
24
+ reason: 'Agreed scope',
25
+ allocations: [
26
+ {
27
+ id: 'allocation-a',
28
+ label: 'Design services',
29
+ amount: '1200.00',
30
+ retained: true,
31
+ sourceMinor: 120000,
32
+ minimumMinor: 0,
33
+ maximumMinor: 120000,
34
+ },
35
+ ],
36
+ });
37
+ let review = $state<PurchaseReviewSnapshot | undefined>();
38
+ let message = $state('');
39
+ let sequence = 1;
40
+ let source = $state({
41
+ id: 'source-1',
42
+ label: 'Quotation Q-104',
43
+ vendor: 'North Studio',
44
+ });
45
+ // Demo-only CAD endpoint parser. Real callers validate using their owning money contract.
46
+ function demoCadMinor(value: string): number | null {
47
+ if (!/^\d+(?:\.\d{1,2})?$/.test(value)) return null;
48
+ const [whole, fraction = ''] = value.split('.');
49
+ const minor = BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0'));
50
+ return minor <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(minor) : null;
51
+ }
52
+ function submit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
53
+ event.preventDefault();
54
+ const data = new FormData(event.currentTarget, event.submitter);
55
+ const text = (name: string) => String(data.get(name) ?? '');
56
+ values = {
57
+ instrument: text('instrument'),
58
+ tax: text('tax'),
59
+ scope: text('scope'),
60
+ inclusions: text('inclusions'),
61
+ exclusions: text('exclusions'),
62
+ reason: text('reason'),
63
+ allocations: data.getAll('allocationId').map((id, index) => ({
64
+ ...values.allocations.find((row) => row.id === id)!,
65
+ amount: String(data.getAll('allocationAmount')[index] ?? ''),
66
+ })),
67
+ };
68
+ const intent = text('intent');
69
+ if (intent === 'addAllocation') {
70
+ values.allocations = [
71
+ ...values.allocations,
72
+ {
73
+ id: `new-${++sequence}`,
74
+ label: t(P['commerce.purchase.demo_additional'], { number: sequence }),
75
+ amount: '',
76
+ },
77
+ ];
78
+ review = undefined;
79
+ message = t(P['commerce.purchase.demo_added']);
80
+ } else if (intent.startsWith('removeAllocation:')) {
81
+ values.allocations = values.allocations.filter(
82
+ (row) => row.id !== intent.slice('removeAllocation:'.length),
83
+ );
84
+ review = undefined;
85
+ message = t(P['commerce.purchase.demo_removed']);
86
+ } else if (intent === 'review') {
87
+ const amounts = values.allocations.map((row) => demoCadMinor(row.amount));
88
+ const taxMinor = demoCadMinor(values.tax);
89
+ const sum = amounts.reduce<bigint>(
90
+ (total, amount) => total + BigInt(amount ?? 0),
91
+ 0n,
92
+ );
93
+ if (
94
+ amounts.some((amount) => amount === null) ||
95
+ taxMinor === null ||
96
+ sum > BigInt(Number.MAX_SAFE_INTEGER) ||
97
+ BigInt(taxMinor) > sum
98
+ ) {
99
+ review = undefined;
100
+ message = t(P['commerce.purchase.demo_invalid']);
101
+ return;
102
+ }
103
+ review = {
104
+ fingerprint: 'demo-review',
105
+ sourceLabel: `${source.vendor} / ${source.label}`,
106
+ total: { currency: 'CAD', minorUnitDigits: 2, amountMinor: Number(sum) },
107
+ taxMinor,
108
+ scope: values.scope,
109
+ notices: [t(P['commerce.purchase.demo_authority'])],
110
+ };
111
+ message = t(P['commerce.purchase.demo_review']);
112
+ } else if (intent === 'edit') {
113
+ review = undefined;
114
+ message = t(P['commerce.purchase.demo_edit']);
115
+ } else {
116
+ message = t(P['commerce.purchase.demo_uncertain']);
117
+ }
118
+ }
119
+ function selectSource(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
120
+ event.preventDefault();
121
+ const id = String(new FormData(event.currentTarget).get('source') ?? '');
122
+ source =
123
+ id === 'source-2'
124
+ ? { id, label: 'Quotation Q-105', vendor: 'South Studio' }
125
+ : { id: 'source-1', label: 'Quotation Q-104', vendor: 'North Studio' };
126
+ review = undefined;
127
+ message = t(P['commerce.purchase.demo_source']);
128
+ }
129
+ </script>
130
+ <p>{t(P['commerce.purchase.demo_notice'])}</p>
131
+ <Button variant="secondary" onclick={()=>{reduction=!reduction;review=undefined;}}>{reduction?t(P['commerce.purchase.demo_amendment']):t(P['commerce.purchase.demo_reduction'])}</Button>
132
+ <PurchaseSourceSelector disabled={reduction} action="" value={source.id} sources={[{id:'source-1',label:'Quotation Q-104',vendor:'North Studio'},{id:'source-2',label:'Quotation Q-105',vendor:'South Studio'},{id:'source-old',label:'Quotation Q-099',vendor:'Prior Vendor',disabled:true,notice:t(P['commerce.purchase.demo_unavailable'])}]} onsubmit={selectSource}/>
133
+ <PurchaseOrderEditor operation={reduction?'reduce':'amend'} {source} currency="CAD" minorUnitDigits={2} {values} instruments={[{value:'purchase-order',label:t(P['commerce.purchase.demo_order'])},{value:'agreement',label:t(P['commerce.purchase.demo_agreement'])}]} action="" {review} {message} hiddenFields={[{name:'requestId',value:'demo-purchase-request'},{name:'sourceId',value:source.id},{name:'predecessorId',value:'order-previous'}]} rowActions={{add:'addAllocation',removePrefix:'removeAllocation:'}} onsubmit={submit}/>
@@ -0,0 +1,9 @@
1
+ /** Interactive purchasing form demo; no record is written. */
2
+ export interface Props {
3
+ /** Show the explicit retained-allocation reduction workflow. */
4
+ reduction?: boolean;
5
+ }
6
+ declare const PurchasingPlayground: import("svelte").Component<Props, {}, "">;
7
+ type PurchasingPlayground = ReturnType<typeof PurchasingPlayground>;
8
+ export default PurchasingPlayground;
9
+ //# sourceMappingURL=PurchasingPlayground.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PurchasingPlayground.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/PurchasingPlayground.svelte.ts"],"names":[],"mappings":"AAaA,8DAA8D;AAC9D,MAAM,WAAW,KAAK;IACpB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAkID,QAAA,MAAM,oBAAoB,2CAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,177 @@
1
+ <script lang="ts">
2
+ import type { CodeSelectOption } from '@happyvertical/smrt-ui/forms';
3
+ import {
4
+ CurrencySelect,
5
+ Form,
6
+ FormGroup,
7
+ Input,
8
+ Select,
9
+ Textarea,
10
+ } from '@happyvertical/smrt-ui/forms';
11
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
12
+ import { Button } from '@happyvertical/smrt-ui/ui';
13
+ import type { Snippet } from 'svelte';
14
+ import type { Attachment } from 'svelte/attachments';
15
+ import type { HTMLFormAttributes } from 'svelte/elements';
16
+ import { Q } from '../quote-i18n.js';
17
+ import {
18
+ type QuoteDocumentKind,
19
+ type QuoteDraftValues,
20
+ type QuoteFieldNames,
21
+ quoteFieldNames,
22
+ } from '../quote-types.js';
23
+
24
+ /** Narrow native attributes and Svelte attachment keys; avoids recursive HTML prop unions. */
25
+ export interface QuoteFormAttributes {
26
+ /** Native form identity and encoding. */
27
+ id?: string;
28
+ /** Optional multipart encoding for file controls composed through extensions. */
29
+ enctype?: HTMLFormAttributes['enctype'];
30
+ /** Native autocomplete setting. */
31
+ autocomplete?: HTMLFormAttributes['autocomplete'];
32
+ /** Svelte attachments, including caller-owned form-retry attachment. */
33
+ [key: symbol]: Attachment<HTMLFormElement>;
34
+ }
35
+
36
+ /** Native quotation/estimate editor; caller retains data, authority and retry identity. */
37
+ export interface Props {
38
+ /** Explicit domain of the document. */
39
+ kind: QuoteDocumentKind;
40
+ /** Server-rendered values, including rejected input strings. */
41
+ values: QuoteDraftValues;
42
+ /** Available authorized counterparties. */
43
+ counterparties: { id: string; label: string }[];
44
+ /** Native form destination; no routes are supplied by Commerce. */
45
+ action: string;
46
+ /** Native method (POST by default). */
47
+ method?: 'post' | 'get';
48
+ /** Caller-owned repeated hidden values, such as tenant/request/predecessor IDs. */
49
+ hiddenFields?: { name: string; value: string }[];
50
+ /** Override payload field names for existing endpoints. */
51
+ names?: Partial<QuoteFieldNames>;
52
+ /** Field errors keyed by canonical names or line ID plus field (e.g. a.description). */
53
+ errors?: Record<string, string>;
54
+ /** Server failure/uncertain outcome explanation; never clears the form. */
55
+ message?: string;
56
+ /** Presentation capability only; the server must enforce authorization. */
57
+ readonly?: boolean;
58
+ /** Disable writes while the caller handles a request. */
59
+ busy?: boolean;
60
+ /** Native submitter name and action values. Removal appends the stable line ID. */
61
+ intents?: { name: string; save: string; add: string; removePrefix: string };
62
+ /** Text overrides, including Client terminology. */
63
+ labels?: { title?: string; counterparty?: string; save?: string };
64
+ /** Caller-restricted currency choices; omitted uses the shared ISO defaults. */
65
+ currencyOptions?: readonly CodeSelectOption[];
66
+ /** Optional navigation destination. */
67
+ cancelHref?: string;
68
+ /** Caller form enhancement; native submission remains enabled unless handler prevents it. */
69
+ onsubmit?: HTMLFormAttributes['onsubmit'];
70
+ /** Caller-controlled form attributes/attachments (e.g. createAttachmentKey for form-retry). */
71
+ formAttributes?: QuoteFormAttributes;
72
+ /** Domain-specific fields, such as allocation or source-selection controls. */
73
+ extensions?: Snippet;
74
+ /** Authorized attachment composition; this component never loads private files. */
75
+ attachments?: Snippet;
76
+ }
77
+ let {
78
+ kind,
79
+ values,
80
+ counterparties,
81
+ action,
82
+ method = 'post',
83
+ hiddenFields = [],
84
+ names = {},
85
+ errors = {},
86
+ message,
87
+ readonly = false,
88
+ busy = false,
89
+ intents = {
90
+ name: 'intent',
91
+ save: 'save',
92
+ add: 'addLine',
93
+ removePrefix: 'removeLine:',
94
+ },
95
+ labels = {},
96
+ currencyOptions,
97
+ cancelHref,
98
+ onsubmit,
99
+ formAttributes = {},
100
+ extensions,
101
+ attachments,
102
+ }: Props = $props();
103
+ const i18n = useI18n();
104
+ const { t } = i18n;
105
+ const fields = $derived({ ...quoteFieldNames, ...names });
106
+ const title = $derived(
107
+ labels.title ??
108
+ t(
109
+ Q[
110
+ kind === 'vendor-quotation'
111
+ ? 'commerce.quote.vendor'
112
+ : 'commerce.quote.estimate'
113
+ ],
114
+ ),
115
+ );
116
+ </script>
117
+
118
+ <section class="quote-editor" aria-label={title}>
119
+ <h2>{title}</h2>
120
+ <p>{t(Q[kind === 'vendor-quotation' ? 'commerce.quote.vendor_hint' : 'commerce.quote.estimate_hint'])}</p>
121
+ {#if readonly}<p>{t(Q['commerce.quote.readonly'])}</p>{/if}
122
+ {#if message}<p role="alert">{message}</p>{/if}
123
+ <Form {...formAttributes} {action} {method} {onsubmit} preventDefault={false}>
124
+ {#each hiddenFields as field}<Input type="hidden" name={field.name} value={field.value} />{/each}
125
+ <fieldset disabled={readonly || busy}>
126
+ <legend>{title}</legend>
127
+ {#if !readonly}<div class="actions"><Button type="submit" name={intents.name} value={intents.save} density="touch">{labels.save ?? t(Q['commerce.quote.save'])}</Button></div>{/if}
128
+ <FormGroup label={labels.counterparty ?? t(Q[kind === 'vendor-quotation' ? 'commerce.quote.vendor_party' : 'commerce.quote.customer_party'])} error={errors.counterpartyId} required>
129
+ <Select name={fields.counterpartyId} value={values.counterpartyId} required density="touch">
130
+ <option value="">{t(Q['commerce.quote.choose'])}</option>
131
+ {#if values.counterpartyId && !counterparties.some(party => party.id === values.counterpartyId)}<option value={values.counterpartyId}>{values.counterpartyId}</option>{/if}
132
+ {#each counterparties as party}<option value={party.id}>{party.label}</option>{/each}
133
+ </Select>
134
+ </FormGroup>
135
+ {#each [{key:'reference',label:'commerce.quote.reference'}, {key:'date',label:'commerce.quote.date'}, {key:'validUntil',label:'commerce.quote.expiry'}, {key:'currency',label:'commerce.quote.currency'}, {key:'total',label:'commerce.quote.total'}, {key:'tax',label:'commerce.quote.tax'}] as field}
136
+ <FormGroup label={t(Q[field.label as keyof typeof Q])} error={errors[field.key]}>
137
+ {#if field.key === 'currency'}
138
+ <CurrencySelect name={fields.currency} value={values.currency} options={currencyOptions} locale={i18n.locale} readOnly={readonly} disabled={busy} density="touch" />
139
+ {:else}
140
+ <Input name={fields[field.key as keyof QuoteFieldNames]} value={values[field.key as 'reference']} inputmode={field.key === 'total' || field.key === 'tax' ? 'decimal' : undefined} density="touch" />
141
+ {/if}
142
+ </FormGroup>
143
+ {/each}
144
+ {#each [{key:'scope',label:'commerce.quote.scope'}, {key:'inclusions',label:'commerce.quote.inclusions'}, {key:'exclusions',label:'commerce.quote.exclusions'}, {key:'reason',label:'commerce.quote.reason'}] as field}
145
+ <FormGroup label={t(Q[field.label as keyof typeof Q])} error={errors[field.key]}>
146
+ <Textarea name={fields[field.key as keyof QuoteFieldNames]} value={values[field.key as 'scope']} />
147
+ </FormGroup>
148
+ {/each}
149
+ <h3>{t(Q['commerce.quote.lines'])}</h3>
150
+ {#each values.lines as line, index (line.id)}
151
+ <fieldset>
152
+ <legend>{t(Q['commerce.quote.line'], {number: index + 1})}</legend>
153
+ <Input type="hidden" name={fields.lineId} value={line.id} />
154
+ <FormGroup label={t(Q['commerce.quote.description'])} error={errors[`${line.id}.description`]}><Input name={fields.lineDescription} value={line.description} density="touch" /></FormGroup>
155
+ <FormGroup label={t(Q['commerce.quote.quantity'])} error={errors[`${line.id}.quantity`]}><Input name={fields.lineQuantity} value={line.quantity} inputmode="decimal" density="touch" /></FormGroup>
156
+ <FormGroup label={t(Q['commerce.quote.rate'])} error={errors[`${line.id}.unitRate`]}><Input name={fields.lineUnitRate} value={line.unitRate} inputmode="decimal" density="touch" /></FormGroup>
157
+ {#if !readonly}<Button type="submit" name={intents.name} value={`${intents.removePrefix}${line.id}`} formnovalidate variant="secondary" density="touch">{t(Q['commerce.quote.remove'], {number: index + 1})}</Button>{/if}
158
+ </fieldset>
159
+ {:else}<p>{t(Q['commerce.quote.empty_lines'])}</p>{/each}
160
+ {#if !readonly}<Button type="submit" name={intents.name} value={intents.add} formnovalidate variant="secondary" density="touch">{t(Q['commerce.quote.add'])}</Button>{/if}
161
+ {@render extensions?.()}
162
+ </fieldset>
163
+ {#if cancelHref}<Button href={cancelHref} variant="secondary" density="touch">{t(Q['commerce.quote.cancel'])}</Button>{/if}
164
+ </Form>
165
+ {#if attachments}
166
+ <fieldset class="editor-attachments" disabled={readonly || busy}>
167
+ {@render attachments()}
168
+ </fieldset>
169
+ {/if}
170
+ </section>
171
+
172
+ <style>
173
+ .editor-attachments { min-width: 0; margin: 0; padding: 0; border: 0; }
174
+ .quote-editor { max-width: 48rem; min-width: 0; color: var(--smrt-color-on-surface); overflow-wrap: anywhere; }
175
+ fieldset { min-width: 0; margin-block: var(--smrt-spacing-4, 1rem); padding: var(--smrt-spacing-4, 1rem); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-md, 0.5rem); }
176
+ .actions { margin-top: var(--smrt-spacing-4, 1rem); }
177
+ </style>
@@ -0,0 +1,76 @@
1
+ import type { CodeSelectOption } from '@happyvertical/smrt-ui/forms';
2
+ import type { Snippet } from 'svelte';
3
+ import type { Attachment } from 'svelte/attachments';
4
+ import type { HTMLFormAttributes } from 'svelte/elements';
5
+ import { type QuoteDocumentKind, type QuoteDraftValues, type QuoteFieldNames } from '../quote-types.js';
6
+ /** Narrow native attributes and Svelte attachment keys; avoids recursive HTML prop unions. */
7
+ export interface QuoteFormAttributes {
8
+ /** Native form identity and encoding. */
9
+ id?: string;
10
+ /** Optional multipart encoding for file controls composed through extensions. */
11
+ enctype?: HTMLFormAttributes['enctype'];
12
+ /** Native autocomplete setting. */
13
+ autocomplete?: HTMLFormAttributes['autocomplete'];
14
+ /** Svelte attachments, including caller-owned form-retry attachment. */
15
+ [key: symbol]: Attachment<HTMLFormElement>;
16
+ }
17
+ /** Native quotation/estimate editor; caller retains data, authority and retry identity. */
18
+ export interface Props {
19
+ /** Explicit domain of the document. */
20
+ kind: QuoteDocumentKind;
21
+ /** Server-rendered values, including rejected input strings. */
22
+ values: QuoteDraftValues;
23
+ /** Available authorized counterparties. */
24
+ counterparties: {
25
+ id: string;
26
+ label: string;
27
+ }[];
28
+ /** Native form destination; no routes are supplied by Commerce. */
29
+ action: string;
30
+ /** Native method (POST by default). */
31
+ method?: 'post' | 'get';
32
+ /** Caller-owned repeated hidden values, such as tenant/request/predecessor IDs. */
33
+ hiddenFields?: {
34
+ name: string;
35
+ value: string;
36
+ }[];
37
+ /** Override payload field names for existing endpoints. */
38
+ names?: Partial<QuoteFieldNames>;
39
+ /** Field errors keyed by canonical names or line ID plus field (e.g. a.description). */
40
+ errors?: Record<string, string>;
41
+ /** Server failure/uncertain outcome explanation; never clears the form. */
42
+ message?: string;
43
+ /** Presentation capability only; the server must enforce authorization. */
44
+ readonly?: boolean;
45
+ /** Disable writes while the caller handles a request. */
46
+ busy?: boolean;
47
+ /** Native submitter name and action values. Removal appends the stable line ID. */
48
+ intents?: {
49
+ name: string;
50
+ save: string;
51
+ add: string;
52
+ removePrefix: string;
53
+ };
54
+ /** Text overrides, including Client terminology. */
55
+ labels?: {
56
+ title?: string;
57
+ counterparty?: string;
58
+ save?: string;
59
+ };
60
+ /** Caller-restricted currency choices; omitted uses the shared ISO defaults. */
61
+ currencyOptions?: readonly CodeSelectOption[];
62
+ /** Optional navigation destination. */
63
+ cancelHref?: string;
64
+ /** Caller form enhancement; native submission remains enabled unless handler prevents it. */
65
+ onsubmit?: HTMLFormAttributes['onsubmit'];
66
+ /** Caller-controlled form attributes/attachments (e.g. createAttachmentKey for form-retry). */
67
+ formAttributes?: QuoteFormAttributes;
68
+ /** Domain-specific fields, such as allocation or source-selection controls. */
69
+ extensions?: Snippet;
70
+ /** Authorized attachment composition; this component never loads private files. */
71
+ attachments?: Snippet;
72
+ }
73
+ declare const QuoteEditor: import("svelte").Component<Props, {}, "">;
74
+ type QuoteEditor = ReturnType<typeof QuoteEditor>;
75
+ export default QuoteEditor;
76
+ //# sourceMappingURL=QuoteEditor.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QuoteEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/QuoteEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAWrE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EAErB,MAAM,mBAAmB,CAAC;AAG3B,8FAA8F;AAC9F,MAAM,WAAW,mBAAmB;IAClC,yCAAyC;IACzC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iFAAiF;IACjF,OAAO,CAAC,EAAE,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACxC,mCAAmC;IACnC,YAAY,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;IAClD,wEAAwE;IACxE,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC;CAC5C;AAED,2FAA2F;AAC3F,MAAM,WAAW,KAAK;IACpB,uCAAuC;IACvC,IAAI,EAAE,iBAAiB,CAAC;IACxB,gEAAgE;IAChE,MAAM,EAAE,gBAAgB,CAAC;IACzB,2CAA2C;IAC3C,cAAc,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChD,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACxB,mFAAmF;IACnF,YAAY,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjD,2DAA2D;IAC3D,KAAK,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACjC,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5E,oDAAoD;IACpD,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE,gFAAgF;IAChF,eAAe,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAC1C,+FAA+F;IAC/F,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mFAAmF;IACnF,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAgHD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
+ import { Button } from '@happyvertical/smrt-ui/ui';
4
+ import { Q } from '../quote-i18n.js';
5
+ import type {
6
+ QuoteDocumentKind,
7
+ QuoteDraftValues,
8
+ QuoteRevision,
9
+ } from '../quote-types.js';
10
+ import QuoteEditor from './QuoteEditor.svelte';
11
+ import QuoteRevisionComparison from './QuoteRevisionComparison.svelte';
12
+ /** Interactive presentation demo; all outcomes are local and no financial record is written. */
13
+ export interface Props {
14
+ /** Start with a vendor quotation or customer estimate. */
15
+ kind?: QuoteDocumentKind;
16
+ }
17
+ let { kind = 'vendor-quotation' }: Props = $props();
18
+ const { t } = useI18n();
19
+ let values = $state<QuoteDraftValues>({
20
+ counterpartyId: 'party-1',
21
+ reference: 'Q-104',
22
+ date: '2026-10-03',
23
+ validUntil: '',
24
+ currency: 'CAD',
25
+ total: '1200.00',
26
+ tax: '60.00',
27
+ scope: 'Supply design services',
28
+ inclusions: 'Two revisions',
29
+ exclusions: 'Printing',
30
+ reason: 'Updated scope',
31
+ lines: [
32
+ {
33
+ id: 'line-1',
34
+ description: 'Design services',
35
+ quantity: '10',
36
+ unitRate: '114.00',
37
+ },
38
+ ],
39
+ });
40
+ let message = $state('');
41
+ let readonly = $state(false);
42
+ let row = 1;
43
+ let errors = $state<Record<string, string>>({});
44
+ const previous = $derived<QuoteRevision>({
45
+ id: 'revision-1',
46
+ kind,
47
+ label: 'Revision 1',
48
+ status: 'Superseded',
49
+ counterparty: 'North Studio',
50
+ total: { currency: 'CAD', minorUnitDigits: 2, amountMinor: 100000 },
51
+ scope: 'Original design scope',
52
+ });
53
+ const current = $derived<QuoteRevision>({
54
+ ...previous,
55
+ id: 'revision-2',
56
+ label: 'Revision 2',
57
+ status: 'Draft',
58
+ total: { currency: 'CAD', minorUnitDigits: 2, amountMinor: 120000 },
59
+ scope: 'Design and two revisions',
60
+ reason: 'Updated scope',
61
+ });
62
+ function submit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
63
+ event.preventDefault();
64
+ const data = new FormData(event.currentTarget, event.submitter);
65
+ const text = (name: string) => String(data.get(name) ?? '');
66
+ const strings = (name: string) => data.getAll(name).map(String);
67
+ const ids = strings('lineId');
68
+ values = {
69
+ counterpartyId: text('counterpartyId'),
70
+ reference: text('reference'),
71
+ date: text('date'),
72
+ validUntil: text('validUntil'),
73
+ currency: text('currency'),
74
+ total: text('total'),
75
+ tax: text('tax'),
76
+ scope: text('scope'),
77
+ inclusions: text('inclusions'),
78
+ exclusions: text('exclusions'),
79
+ reason: text('reason'),
80
+ lines: ids.map((id, index) => ({
81
+ id,
82
+ description: strings('lineDescription')[index] ?? '',
83
+ quantity: strings('lineQuantity')[index] ?? '',
84
+ unitRate: strings('lineUnitRate')[index] ?? '',
85
+ })),
86
+ };
87
+ const intent = text('intent');
88
+ if (intent === 'addLine') {
89
+ values.lines = [
90
+ ...values.lines,
91
+ { id: `line-${++row}`, description: '', quantity: '', unitRate: '' },
92
+ ];
93
+ message = t(Q['commerce.quote.demo_added']);
94
+ } else if (intent.startsWith('removeLine:')) {
95
+ values.lines = values.lines.filter(
96
+ (line) => line.id !== intent.slice('removeLine:'.length),
97
+ );
98
+ message = t(Q['commerce.quote.demo_removed']);
99
+ } else {
100
+ errors = { total: t(Q['commerce.quote.demo_error']) };
101
+ message = t(Q['commerce.quote.demo_rejected']);
102
+ }
103
+ }
104
+ </script>
105
+ <p>{t(Q['commerce.quote.demo_notice'])}</p>
106
+ <Button variant="secondary" onclick={() => {readonly=!readonly;}}>{t(Q[readonly ? 'commerce.quote.demo_enable' : 'commerce.quote.demo_readonly'])}</Button>
107
+ <QuoteEditor {kind} {values} counterparties={[{id:'party-1',label:'North Studio'}]} action="" {errors} {message} {readonly} hiddenFields={[{name:'requestId',value:'demo-request-1'},{name:'expectedTenantId',value:'demo-tenant'}]} onsubmit={submit} />
108
+ <QuoteRevisionComparison {previous} {current} />
@@ -0,0 +1,10 @@
1
+ import type { QuoteDocumentKind } from '../quote-types.js';
2
+ /** Interactive presentation demo; all outcomes are local and no financial record is written. */
3
+ export interface Props {
4
+ /** Start with a vendor quotation or customer estimate. */
5
+ kind?: QuoteDocumentKind;
6
+ }
7
+ declare const QuotePlayground: import("svelte").Component<Props, {}, "">;
8
+ type QuotePlayground = ReturnType<typeof QuotePlayground>;
9
+ export default QuotePlayground;
10
+ //# sourceMappingURL=QuotePlayground.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QuotePlayground.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/QuotePlayground.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,iBAAiB,EAGlB,MAAM,mBAAmB,CAAC;AAI3B,gGAAgG;AAChG,MAAM,WAAW,KAAK;IACpB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAwGD,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
+ import { Q } from '../quote-i18n.js';
4
+ import {
5
+ type QuoteRevision,
6
+ quoteMinorText,
7
+ quoteRevisionDelta,
8
+ } from '../quote-types.js';
9
+ import QuoteRevisionHistory from './QuoteRevisionHistory.svelte';
10
+ /** Compare retained versions without selecting prices, accepting estimates or awarding work. */
11
+ export interface Props {
12
+ /** Earlier retained version. */
13
+ previous: QuoteRevision;
14
+ /** Later retained version. */
15
+ current: QuoteRevision;
16
+ /** Optional localized heading. */
17
+ title?: string;
18
+ }
19
+ let { previous, current, title }: Props = $props();
20
+ const { t } = useI18n();
21
+ const delta = $derived.by(() => {
22
+ try {
23
+ const money = quoteRevisionDelta(previous, current);
24
+ return money ? { money, text: quoteMinorText(money) } : null;
25
+ } catch (error) {
26
+ if (error instanceof RangeError) return undefined;
27
+ throw error;
28
+ }
29
+ });
30
+ </script>
31
+ <section class="quote-comparison" aria-label={title ?? t(Q['commerce.quote.compare'])}>
32
+ <h2>{title ?? t(Q['commerce.quote.compare'])}</h2>
33
+ <div class="versions">
34
+ <QuoteRevisionHistory revisions={[previous]} title={t(Q['commerce.quote.before'])} />
35
+ <QuoteRevisionHistory revisions={[current]} title={t(Q['commerce.quote.after'])} />
36
+ </div>
37
+ {#if delta}<p>{t(Q['commerce.quote.delta'])}: {delta.text} {delta.money.currency}</p>
38
+ {:else if delta === undefined}<p>{t(Q['commerce.quote.unavailable'])}</p>
39
+ {:else}<p>{t(Q['commerce.quote.incomparable'])}</p>{/if}
40
+ </section>
41
+ <style>
42
+ .quote-comparison { min-width: 0; color: var(--smrt-color-on-surface); overflow-wrap: anywhere; }
43
+ .versions { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: var(--smrt-spacing-4, 1rem); }
44
+ </style>
@@ -0,0 +1,14 @@
1
+ import { type QuoteRevision } from '../quote-types.js';
2
+ /** Compare retained versions without selecting prices, accepting estimates or awarding work. */
3
+ export interface Props {
4
+ /** Earlier retained version. */
5
+ previous: QuoteRevision;
6
+ /** Later retained version. */
7
+ current: QuoteRevision;
8
+ /** Optional localized heading. */
9
+ title?: string;
10
+ }
11
+ declare const QuoteRevisionComparison: import("svelte").Component<Props, {}, "">;
12
+ type QuoteRevisionComparison = ReturnType<typeof QuoteRevisionComparison>;
13
+ export default QuoteRevisionComparison;
14
+ //# sourceMappingURL=QuoteRevisionComparison.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QuoteRevisionComparison.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/QuoteRevisionComparison.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAC;AAG3B,gGAAgG;AAChG,MAAM,WAAW,KAAK;IACpB,gCAAgC;IAChC,QAAQ,EAAE,aAAa,CAAC;IACxB,8BAA8B;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiCD,QAAA,MAAM,uBAAuB,2CAAwC,CAAC;AACtE,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAC1E,eAAe,uBAAuB,CAAC"}
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
+ import { Button } from '@happyvertical/smrt-ui/ui';
4
+ import type { Snippet } from 'svelte';
5
+ import { Q } from '../quote-i18n.js';
6
+ import {
7
+ type QuoteMoneyMinor,
8
+ type QuoteRevision,
9
+ quoteMinorText,
10
+ } from '../quote-types.js';
11
+ /** Read-only revision evidence, in caller-provided order. */
12
+ export interface Props {
13
+ /** Immutable summaries, including unknown caller status labels. */
14
+ revisions: QuoteRevision[];
15
+ /** Optional localized heading. */
16
+ title?: string;
17
+ /** Optional authorized evidence/details for each revision. */
18
+ evidence?: Snippet<[QuoteRevision]>;
19
+ }
20
+ let { revisions, title, evidence }: Props = $props();
21
+ const { t } = useI18n();
22
+ function amount(money: QuoteMoneyMinor): string {
23
+ try {
24
+ return quoteMinorText(money) ?? t(Q['commerce.quote.unknown']);
25
+ } catch (error) {
26
+ if (error instanceof RangeError) return t(Q['commerce.quote.unavailable']);
27
+ throw error;
28
+ }
29
+ }
30
+ </script>
31
+ <section class="quote-history" aria-label={title ?? t(Q['commerce.quote.history'])}>
32
+ <h2>{title ?? t(Q['commerce.quote.history'])}</h2>
33
+ <ol>
34
+ {#each revisions as revision (revision.id)}
35
+ <li>
36
+ <article>
37
+ <h3>{revision.label}</h3>
38
+ <p>{t(Q[revision.kind === 'vendor-quotation' ? 'commerce.quote.vendor' : 'commerce.quote.estimate'])} · {revision.counterparty}</p>
39
+ <p>{revision.status}{#if revision.recordedAt} · {revision.recordedAt}{/if}</p>
40
+ <p>{amount(revision.total)} {revision.total.currency}</p>
41
+ {#if revision.reason}<p>{revision.reason}</p>{/if}
42
+ {#if revision.scope}<p>{revision.scope}</p>{/if}
43
+ {@render evidence?.(revision)}
44
+ {#if revision.href}<Button href={revision.href} variant="secondary" density="touch">{t(Q['commerce.quote.view'])}</Button>{/if}
45
+ </article>
46
+ </li>
47
+ {:else}<li>{t(Q['commerce.quote.empty_history'])}</li>{/each}
48
+ </ol>
49
+ </section>
50
+ <style>
51
+ .quote-history { min-width: 0; color: var(--smrt-color-on-surface); overflow-wrap: anywhere; }
52
+ ol { padding-inline-start: var(--smrt-spacing-6, 1.5rem); }
53
+ li { margin-block: var(--smrt-spacing-4, 1rem); }
54
+ article { padding: var(--smrt-spacing-4, 1rem); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-md, 0.5rem); }
55
+ </style>