@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.
- package/AGENTS.md +19 -3
- package/README.md +29 -1
- package/agents/commercial-ui.md +78 -0
- package/agents/customer-vendor-ui.md +76 -0
- package/agents/invoice-calculations.md +17 -0
- package/agents/invoice-preparation.md +86 -0
- package/agents/invoice-quantity.md +46 -0
- package/agents/pricing-versions.md +28 -0
- package/agents/purchasing-ui.md +82 -0
- package/agents/quote-ui.md +82 -0
- package/dist/billing/units.d.ts +1 -7
- package/dist/billing/units.d.ts.map +1 -1
- package/dist/chunks/calculations-DPuldr1J.js +242 -0
- package/dist/chunks/calculations-DPuldr1J.js.map +1 -0
- package/dist/chunks/types-CRnln577.js.map +1 -1
- package/dist/chunks/{units-CpBepqG1.js → units-CJcAOQm1.js} +3 -39
- package/dist/chunks/units-CJcAOQm1.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +164 -44
- package/dist/index.js.map +1 -1
- package/dist/invoices.d.ts +3 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +2 -0
- package/dist/manifest.json +66 -8
- package/dist/migrations/invoiceEditorStorage.d.ts +39 -0
- package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -0
- package/dist/models/ContractLineItem.d.ts +1 -1
- package/dist/models/InvoiceLineItem.d.ts +18 -36
- package/dist/models/InvoiceLineItem.d.ts.map +1 -1
- package/dist/playground.js +201 -3
- package/dist/playground.js.map +1 -1
- package/dist/smrt-knowledge.json +109 -7
- package/dist/svelte/components/CustomerDetail.svelte +150 -0
- package/dist/svelte/components/CustomerDetail.svelte.d.ts +28 -0
- package/dist/svelte/components/CustomerDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/components/CustomerDirectory.svelte +177 -0
- package/dist/svelte/components/CustomerDirectory.svelte.d.ts +42 -0
- package/dist/svelte/components/CustomerDirectory.svelte.d.ts.map +1 -0
- package/dist/svelte/components/CustomerForm.svelte +234 -0
- package/dist/svelte/components/CustomerForm.svelte.d.ts +44 -0
- package/dist/svelte/components/CustomerForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceAllocationFields.svelte +72 -0
- package/dist/svelte/components/InvoiceAllocationFields.svelte.d.ts +8 -0
- package/dist/svelte/components/InvoiceAllocationFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceCard.svelte +2 -2
- package/dist/svelte/components/InvoiceEditor.svelte +134 -0
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts +8 -0
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceLineEditor.svelte +85 -0
- package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +8 -0
- package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceReview.svelte +23 -0
- package/dist/svelte/components/InvoiceReview.svelte.d.ts +10 -0
- package/dist/svelte/components/InvoiceReview.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PartyAddressFields.svelte +95 -0
- package/dist/svelte/components/PartyAddressFields.svelte.d.ts +25 -0
- package/dist/svelte/components/PartyAddressFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PartyContactFields.svelte +153 -0
- package/dist/svelte/components/PartyContactFields.svelte.d.ts +28 -0
- package/dist/svelte/components/PartyContactFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PricingVersionDecision.svelte +88 -0
- package/dist/svelte/components/PricingVersionDecision.svelte.d.ts +35 -0
- package/dist/svelte/components/PricingVersionDecision.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PricingVersionSummary.svelte +46 -0
- package/dist/svelte/components/PricingVersionSummary.svelte.d.ts +9 -0
- package/dist/svelte/components/PricingVersionSummary.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte +210 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +79 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchaseSourceSelector.svelte +56 -0
- package/dist/svelte/components/PurchaseSourceSelector.svelte.d.ts +30 -0
- package/dist/svelte/components/PurchaseSourceSelector.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchasingPlayground.svelte +133 -0
- package/dist/svelte/components/PurchasingPlayground.svelte.d.ts +9 -0
- package/dist/svelte/components/PurchasingPlayground.svelte.d.ts.map +1 -0
- package/dist/svelte/components/QuoteEditor.svelte +177 -0
- package/dist/svelte/components/QuoteEditor.svelte.d.ts +76 -0
- package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/QuotePlayground.svelte +108 -0
- package/dist/svelte/components/QuotePlayground.svelte.d.ts +10 -0
- package/dist/svelte/components/QuotePlayground.svelte.d.ts.map +1 -0
- package/dist/svelte/components/QuoteRevisionComparison.svelte +44 -0
- package/dist/svelte/components/QuoteRevisionComparison.svelte.d.ts +14 -0
- package/dist/svelte/components/QuoteRevisionComparison.svelte.d.ts.map +1 -0
- package/dist/svelte/components/QuoteRevisionHistory.svelte +55 -0
- package/dist/svelte/components/QuoteRevisionHistory.svelte.d.ts +15 -0
- package/dist/svelte/components/QuoteRevisionHistory.svelte.d.ts.map +1 -0
- package/dist/svelte/components/VendorDetail.svelte +134 -0
- package/dist/svelte/components/VendorDetail.svelte.d.ts +26 -0
- package/dist/svelte/components/VendorDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/components/VendorDirectory.svelte +178 -0
- package/dist/svelte/components/VendorDirectory.svelte.d.ts +42 -0
- package/dist/svelte/components/VendorDirectory.svelte.d.ts.map +1 -0
- package/dist/svelte/components/VendorForm.svelte +224 -0
- package/dist/svelte/components/VendorForm.svelte.d.ts +42 -0
- package/dist/svelte/components/VendorForm.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +102 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +102 -0
- package/dist/svelte/index.d.ts +52 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +49 -0
- package/dist/svelte/invoices/calculations.d.ts +121 -0
- package/dist/svelte/invoices/calculations.d.ts.map +1 -0
- package/dist/svelte/invoices/calculations.js +259 -0
- package/dist/svelte/invoices/currency.d.ts +9 -0
- package/dist/svelte/invoices/currency.d.ts.map +1 -0
- package/dist/svelte/invoices/currency.js +55 -0
- package/dist/svelte/invoices/fields.d.ts +6 -0
- package/dist/svelte/invoices/fields.d.ts.map +1 -0
- package/dist/svelte/invoices/fields.js +22 -0
- package/dist/svelte/invoices/messages.d.ts +53 -0
- package/dist/svelte/invoices/messages.d.ts.map +1 -0
- package/dist/svelte/invoices/messages.js +53 -0
- package/dist/svelte/invoices/types.d.ts +194 -0
- package/dist/svelte/invoices/types.d.ts.map +1 -0
- package/dist/svelte/invoices/types.js +1 -0
- package/dist/svelte/party-types.d.ts +203 -0
- package/dist/svelte/party-types.d.ts.map +1 -0
- package/dist/svelte/party-types.js +82 -0
- package/dist/svelte/playground/InvoicePreparationPreview.svelte +116 -0
- package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/PartyFormPreview.svelte +211 -0
- package/dist/svelte/playground/PartyFormPreview.svelte.d.ts +8 -0
- package/dist/svelte/playground/PartyFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/PricingVersionPreview.svelte +55 -0
- package/dist/svelte/playground/PricingVersionPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/PricingVersionPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +442 -3
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +199 -3
- package/dist/svelte/pricing-i18n.d.ts +10 -0
- package/dist/svelte/pricing-i18n.d.ts.map +1 -0
- package/dist/svelte/pricing-i18n.js +10 -0
- package/dist/svelte/pricing-types.d.ts +38 -0
- package/dist/svelte/pricing-types.d.ts.map +1 -0
- package/dist/svelte/pricing-types.js +1 -0
- package/dist/svelte/purchasing-i18n.d.ts +53 -0
- package/dist/svelte/purchasing-i18n.d.ts.map +1 -0
- package/dist/svelte/purchasing-i18n.js +53 -0
- package/dist/svelte/purchasing-types.d.ts +91 -0
- package/dist/svelte/purchasing-types.d.ts.map +1 -0
- package/dist/svelte/purchasing-types.js +13 -0
- package/dist/svelte/quote-i18n.d.ts +49 -0
- package/dist/svelte/quote-i18n.d.ts.map +1 -0
- package/dist/svelte/quote-i18n.js +49 -0
- package/dist/svelte/quote-types.d.ts +81 -0
- package/dist/svelte/quote-types.d.ts.map +1 -0
- package/dist/svelte/quote-types.js +51 -0
- package/dist/types/index.d.ts +13 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +150 -6
- package/dist/ui.js.map +1 -1
- package/package.json +25 -13
- package/dist/chunks/units-CpBepqG1.js.map +0 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { CurrencyDisplay } from '@happyvertical/smrt-ui';
|
|
3
|
+
import {
|
|
4
|
+
CurrencySelect,
|
|
5
|
+
Form,
|
|
6
|
+
FormGroup,
|
|
7
|
+
Input,
|
|
8
|
+
Select,
|
|
9
|
+
} from '@happyvertical/smrt-ui/forms';
|
|
10
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
11
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
12
|
+
import { calculateInvoiceDraft } from '../invoices/calculations.js';
|
|
13
|
+
import {
|
|
14
|
+
invoiceFieldNames,
|
|
15
|
+
invoiceLineFieldNames,
|
|
16
|
+
} from '../invoices/fields.js';
|
|
17
|
+
import { invoicePreparationMessages as M } from '../invoices/messages.js';
|
|
18
|
+
import type {
|
|
19
|
+
InvoiceDraftValues,
|
|
20
|
+
InvoiceEditorProps,
|
|
21
|
+
InvoiceLineFieldNames,
|
|
22
|
+
} from '../invoices/types.js';
|
|
23
|
+
import InvoiceLineEditor from './InvoiceLineEditor.svelte';
|
|
24
|
+
import InvoiceReview from './InvoiceReview.svelte';
|
|
25
|
+
/** General native invoice editor; server owns prices, tax, authorization and persistence. */
|
|
26
|
+
export interface Props extends InvoiceEditorProps {}
|
|
27
|
+
let {
|
|
28
|
+
action,
|
|
29
|
+
method = 'post',
|
|
30
|
+
id,
|
|
31
|
+
title,
|
|
32
|
+
values,
|
|
33
|
+
customers = [],
|
|
34
|
+
errors = {},
|
|
35
|
+
fields = {},
|
|
36
|
+
lineFields = {},
|
|
37
|
+
canEdit = true,
|
|
38
|
+
pending = false,
|
|
39
|
+
retryStatus = 'idle',
|
|
40
|
+
message,
|
|
41
|
+
review,
|
|
42
|
+
hiddenFields = [],
|
|
43
|
+
saveIntent = 'save',
|
|
44
|
+
addIntent = 'addLine',
|
|
45
|
+
removePrefix = 'removeLine:',
|
|
46
|
+
saveLabel,
|
|
47
|
+
cancelHref,
|
|
48
|
+
children,
|
|
49
|
+
onsubmit,
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
const { t } = useI18n();
|
|
52
|
+
const instanceId = $props.id();
|
|
53
|
+
const formId = $derived(id ?? `invoice-editor-${instanceId}`);
|
|
54
|
+
const names = $derived({ ...invoiceFieldNames, ...fields });
|
|
55
|
+
const rowNames = $derived({ ...invoiceLineFieldNames, ...lineFields });
|
|
56
|
+
const busy = $derived(
|
|
57
|
+
pending || retryStatus === 'submitting' || retryStatus === 'busy',
|
|
58
|
+
);
|
|
59
|
+
let draft = $derived(values);
|
|
60
|
+
const calculation = $derived(calculateInvoiceDraft(draft));
|
|
61
|
+
function refresh(event: Event) {
|
|
62
|
+
const data = new FormData(event.currentTarget as HTMLFormElement);
|
|
63
|
+
const text = (name: string) => String(data.get(name) ?? '');
|
|
64
|
+
draft = {
|
|
65
|
+
...draft,
|
|
66
|
+
...Object.fromEntries(
|
|
67
|
+
Object.entries(names)
|
|
68
|
+
.filter(([key]) => key !== 'intent')
|
|
69
|
+
.map(([key, name]) => [key, text(name)]),
|
|
70
|
+
),
|
|
71
|
+
lines: draft.lines.map((line, index) => ({
|
|
72
|
+
...line,
|
|
73
|
+
...Object.fromEntries(
|
|
74
|
+
Object.entries(rowNames).map(([key, name]) => [
|
|
75
|
+
key,
|
|
76
|
+
String(data.getAll(name)[index] ?? ''),
|
|
77
|
+
]),
|
|
78
|
+
),
|
|
79
|
+
})),
|
|
80
|
+
} as InvoiceDraftValues;
|
|
81
|
+
}
|
|
82
|
+
function lineErrors(key: string) {
|
|
83
|
+
return Object.fromEntries(
|
|
84
|
+
Object.keys(rowNames).map((field) => [field, errors[`${key}.${field}`]]),
|
|
85
|
+
) as Partial<Record<keyof InvoiceLineFieldNames, string>>;
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
<section class="invoice-editor" aria-labelledby={`${formId}-title`}>
|
|
89
|
+
<h2 id={`${formId}-title`}>{title ?? t(M['commerce.preparation.title'])}</h2>
|
|
90
|
+
{#if review}<InvoiceReview {review} />{/if}
|
|
91
|
+
{#if !canEdit}<p>{t(M['commerce.preparation.readonly'])}</p>{/if}
|
|
92
|
+
<Form id={formId} {action} {method} preventDefault={false} {onsubmit} oninput={refresh} onchange={refresh} aria-busy={busy}>
|
|
93
|
+
<div class="fields">
|
|
94
|
+
{#if message}<p role="alert">{message}</p>{/if}
|
|
95
|
+
{#if retryStatus === 'transport-error'}<p role="status">{t(M['commerce.preparation.uncertain'])}</p>{/if}
|
|
96
|
+
{#each hiddenFields as field}<Input type="hidden" name={field.name} value={field.value} interaction={false} />{/each}
|
|
97
|
+
{#if canEdit}<Button type="submit" name={names.intent} value={saveIntent} disabled={busy}>{saveLabel ?? t(M['commerce.preparation.save'])}</Button>{/if}
|
|
98
|
+
<fieldset disabled={!canEdit} class="header-fields">
|
|
99
|
+
<FormGroup id={`${formId}-customer`} label={t(M['commerce.invoice.customer'])} error={errors.customerId}>
|
|
100
|
+
<Select name={names.customerId} value={draft.customerId}>
|
|
101
|
+
<option value="">{t(M['commerce.invoice.chooseCustomer'])}</option>
|
|
102
|
+
{#if draft.customerId && !customers.some(customer => customer.id === draft.customerId)}<option value={draft.customerId}>{t(M['commerce.invoice.retained'], { reference: draft.customerId })}</option>{/if}
|
|
103
|
+
{#each customers as customer}<option value={customer.id}>{customer.label}</option>{/each}
|
|
104
|
+
</Select>
|
|
105
|
+
</FormGroup>
|
|
106
|
+
<FormGroup id={`${formId}-issued`} label={t(M['commerce.invoice.issued'])} error={errors.issuedOn}><Input name={names.issuedOn} value={draft.issuedOn} /></FormGroup>
|
|
107
|
+
<FormGroup id={`${formId}-due`} label={t(M['commerce.invoice.due'])} error={errors.dueOn}><Input name={names.dueOn} value={draft.dueOn} /></FormGroup>
|
|
108
|
+
<FormGroup id={`${formId}-currency`} label={t(M['commerce.invoice.currency'])} error={errors.currency}><CurrencySelect name={names.currency} value={draft.currency} /></FormGroup>
|
|
109
|
+
<FormGroup id={`${formId}-terms`} label={t(M['commerce.invoice.terms'])} error={errors.paymentTerms}><Input name={names.paymentTerms} value={draft.paymentTerms} /></FormGroup>
|
|
110
|
+
<FormGroup id={`${formId}-tax`} label={t(M['commerce.invoice.defaultTax'])} error={errors.taxRate}><Input name={names.taxRate} value={draft.taxRate} inputmode="decimal" /></FormGroup>
|
|
111
|
+
</fieldset>
|
|
112
|
+
{#if draft.lines.length === 0}<p>{t(M['commerce.invoice.empty'])}</p>{/if}
|
|
113
|
+
{#each draft.lines as line, index (line.key)}
|
|
114
|
+
<InvoiceLineEditor {line} currency={draft.currency} inheritedTaxRate={draft.taxRate} number={index + 1} id={`${formId}-line-${index}`} fields={rowNames} errors={lineErrors(line.key)} {canEdit} pending={busy} intentField={names.intent} {removePrefix} />
|
|
115
|
+
{/each}
|
|
116
|
+
{#if canEdit}<Button type="submit" name={names.intent} value={addIntent} formnovalidate disabled={busy} variant="secondary">{t(M['commerce.invoice.add'])}</Button>{/if}
|
|
117
|
+
<div aria-live="polite" class="summary">
|
|
118
|
+
{#if calculation.valid}
|
|
119
|
+
<dl>{#each ['gross', 'discount', 'subtotal', 'tax', 'total'] as key}<div><dt>{t(M[`commerce.invoice.${key}` as 'commerce.invoice.total'])}</dt><dd><CurrencyDisplay amount={calculation[`${key}Minor` as 'totalMinor']} currency={draft.currency} unit="cents" /></dd></div>{/each}</dl>
|
|
120
|
+
{:else}<p>{t(M['commerce.invoice.invalid'])}</p>{/if}
|
|
121
|
+
</div>
|
|
122
|
+
<p>{t(M['commerce.invoice.preview'])}</p>
|
|
123
|
+
{@render children?.(draft)}
|
|
124
|
+
{#if cancelHref}<Button href={cancelHref} variant="ghost">{t(M['commerce.preparation.cancel'])}</Button>{/if}
|
|
125
|
+
</div>
|
|
126
|
+
</Form>
|
|
127
|
+
</section>
|
|
128
|
+
<style>
|
|
129
|
+
.invoice-editor, .fields, .header-fields { display: grid; grid-template-columns: minmax(0,1fr); gap: var(--smrt-spacing-4); min-width: 0; }
|
|
130
|
+
.invoice-editor { max-width: 48rem; overflow-wrap: anywhere; }
|
|
131
|
+
.header-fields { margin: 0; padding: 0; border: 0; }
|
|
132
|
+
h2, p, dl, dd { margin: 0; }
|
|
133
|
+
.summary dl { display: flex; flex-wrap: wrap; gap: var(--smrt-spacing-4); }
|
|
134
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InvoiceEditorProps } from '../invoices/types.js';
|
|
2
|
+
/** General native invoice editor; server owns prices, tax, authorization and persistence. */
|
|
3
|
+
export interface Props extends InvoiceEditorProps {
|
|
4
|
+
}
|
|
5
|
+
declare const InvoiceEditor: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type InvoiceEditor = ReturnType<typeof InvoiceEditor>;
|
|
7
|
+
export default InvoiceEditor;
|
|
8
|
+
//# sourceMappingURL=InvoiceEditor.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InvoiceEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InvoiceEditor.svelte.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAEV,kBAAkB,EAEnB,MAAM,sBAAsB,CAAC;AAI9B,6FAA6F;AAC7F,MAAM,WAAW,KAAM,SAAQ,kBAAkB;CAAG;AAsHpD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { CurrencyDisplay } from '@happyvertical/smrt-ui';
|
|
3
|
+
import { FormGroup, Input, Select } from '@happyvertical/smrt-ui/forms';
|
|
4
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
5
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
6
|
+
import { calculateInvoiceLine } from '../invoices/calculations.js';
|
|
7
|
+
import { invoiceLineFieldNames } from '../invoices/fields.js';
|
|
8
|
+
import { invoicePreparationMessages as M } from '../invoices/messages.js';
|
|
9
|
+
import type { InvoiceLineEditorProps } from '../invoices/types.js';
|
|
10
|
+
/** General invoice line fields and exact non-authoritative calculation preview. */
|
|
11
|
+
export interface Props extends InvoiceLineEditorProps {}
|
|
12
|
+
let {
|
|
13
|
+
line,
|
|
14
|
+
currency,
|
|
15
|
+
inheritedTaxRate,
|
|
16
|
+
number = 1,
|
|
17
|
+
id,
|
|
18
|
+
fields = {},
|
|
19
|
+
errors = {},
|
|
20
|
+
canEdit = true,
|
|
21
|
+
pending = false,
|
|
22
|
+
intentField = 'intent',
|
|
23
|
+
removePrefix = 'removeLine:',
|
|
24
|
+
onchange,
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
const { t } = useI18n();
|
|
27
|
+
const instanceId = $props.id();
|
|
28
|
+
const prefix = $derived(id ?? `invoice-line-${instanceId}`);
|
|
29
|
+
const names = $derived({ ...invoiceLineFieldNames, ...fields });
|
|
30
|
+
let draft = $derived(line);
|
|
31
|
+
const calculation = $derived(
|
|
32
|
+
calculateInvoiceLine(draft, { currency, inheritedTaxRate }),
|
|
33
|
+
);
|
|
34
|
+
function refresh(event: Event) {
|
|
35
|
+
const controls = (event.currentTarget as HTMLFieldSetElement).elements;
|
|
36
|
+
draft = {
|
|
37
|
+
...draft,
|
|
38
|
+
...Object.fromEntries(
|
|
39
|
+
Object.entries(names).map(([key, name]) => [
|
|
40
|
+
key,
|
|
41
|
+
(controls.namedItem(name) as HTMLInputElement | null)?.value ??
|
|
42
|
+
draft[key as keyof typeof draft],
|
|
43
|
+
]),
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
onchange?.(draft);
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
<fieldset oninput={refresh} onchange={refresh} disabled={!canEdit} class="invoice-line">
|
|
50
|
+
<legend>{t(M['commerce.invoice.line'], { number })}</legend>
|
|
51
|
+
<Input type="hidden" name={names.key} value={draft.key} interaction={false} />
|
|
52
|
+
{#each ['description', 'sku', 'quantity', 'unitPrice'] as field}
|
|
53
|
+
<FormGroup id={`${prefix}-${field}`} label={t(M[`commerce.invoice.${field}` as 'commerce.invoice.description'], { currency })} error={errors[field as keyof typeof errors]}>
|
|
54
|
+
<Input name={names[field as keyof typeof names]} value={draft[field as 'description']} inputmode={field === 'quantity' || field === 'unitPrice' ? 'decimal' : undefined} />
|
|
55
|
+
</FormGroup>
|
|
56
|
+
{/each}
|
|
57
|
+
<FormGroup id={`${prefix}-discountType`} label={t(M['commerce.invoice.discountType'])} error={errors.discountType}>
|
|
58
|
+
<Select name={names.discountType} value={draft.discountType}>
|
|
59
|
+
{#if draft.discountType !== 'flat' && draft.discountType !== 'percent'}<option value={draft.discountType}>{draft.discountType}</option>{/if}
|
|
60
|
+
<option value="flat">{t(M['commerce.invoice.flat'])}</option><option value="percent">{t(M['commerce.invoice.percent'])}</option>
|
|
61
|
+
</Select>
|
|
62
|
+
</FormGroup>
|
|
63
|
+
<FormGroup id={`${prefix}-discountValue`} label={t(M['commerce.invoice.discountValue'])} error={errors.discountValue}><Input name={names.discountValue} value={draft.discountValue} inputmode="decimal" /></FormGroup>
|
|
64
|
+
<FormGroup id={`${prefix}-taxMode`} label={t(M['commerce.invoice.taxMode'])} error={errors.taxMode}>
|
|
65
|
+
<Select name={names.taxMode} value={draft.taxMode}>
|
|
66
|
+
{#if draft.taxMode !== 'inherit' && draft.taxMode !== 'override'}<option value={draft.taxMode}>{draft.taxMode}</option>{/if}
|
|
67
|
+
<option value="inherit">{t(M['commerce.invoice.inherit'])}</option><option value="override">{t(M['commerce.invoice.override'])}</option>
|
|
68
|
+
</Select>
|
|
69
|
+
</FormGroup>
|
|
70
|
+
<FormGroup id={`${prefix}-taxRate`} label={t(M['commerce.invoice.taxRate'])} error={errors.taxRate}><Input name={names.taxRate} value={draft.taxRate} inputmode="decimal" /></FormGroup>
|
|
71
|
+
{#if calculation.valid}
|
|
72
|
+
<dl class="totals">
|
|
73
|
+
<div><dt>{t(M['commerce.invoice.subtotal'])}</dt><dd><CurrencyDisplay amount={calculation.subtotalMinor} {currency} unit="cents" /></dd></div>
|
|
74
|
+
<div><dt>{t(M['commerce.invoice.tax'])}</dt><dd><CurrencyDisplay amount={calculation.taxMinor} {currency} unit="cents" /></dd></div>
|
|
75
|
+
<div><dt>{t(M['commerce.invoice.total'])}</dt><dd><CurrencyDisplay amount={calculation.totalMinor} {currency} unit="cents" /></dd></div>
|
|
76
|
+
</dl>
|
|
77
|
+
{:else}<p role="status">{t(M['commerce.invoice.invalid'])}</p>{/if}
|
|
78
|
+
{#if canEdit}<Button type="submit" name={intentField} value={`${removePrefix}${draft.key}`} formnovalidate disabled={pending} variant="secondary">{t(M['commerce.invoice.remove'])}</Button>{/if}
|
|
79
|
+
</fieldset>
|
|
80
|
+
<style>
|
|
81
|
+
.invoice-line { display: grid; grid-template-columns: minmax(0,1fr); gap: var(--smrt-spacing-3); min-width: 0; margin: 0; padding: var(--smrt-spacing-4); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); }
|
|
82
|
+
legend { padding-inline: var(--smrt-spacing-2); }
|
|
83
|
+
.totals { display: flex; flex-wrap: wrap; gap: var(--smrt-spacing-4); }
|
|
84
|
+
dd { margin: 0; } p, dl { margin: 0; }
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InvoiceLineEditorProps } from '../invoices/types.js';
|
|
2
|
+
/** General invoice line fields and exact non-authoritative calculation preview. */
|
|
3
|
+
export interface Props extends InvoiceLineEditorProps {
|
|
4
|
+
}
|
|
5
|
+
declare const InvoiceLineEditor: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type InvoiceLineEditor = ReturnType<typeof InvoiceLineEditor>;
|
|
7
|
+
export default InvoiceLineEditor;
|
|
8
|
+
//# sourceMappingURL=InvoiceLineEditor.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InvoiceLineEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InvoiceLineEditor.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEnE,mFAAmF;AACnF,MAAM,WAAW,KAAM,SAAQ,sBAAsB;CAAG;AAmFxD,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
3
|
+
import { invoicePreparationMessages as M } from '../invoices/messages.js';
|
|
4
|
+
import type { InvoicePreparationReview } from '../invoices/types.js';
|
|
5
|
+
|
|
6
|
+
/** Caller-authoritative approval/review presentation, without mutation actions. */
|
|
7
|
+
export interface Props {
|
|
8
|
+
/** Server snapshot, including caller-localized unknown states. */
|
|
9
|
+
review: InvoicePreparationReview;
|
|
10
|
+
}
|
|
11
|
+
let { review }: Props = $props();
|
|
12
|
+
const { t } = useI18n();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<section aria-label={t(M['commerce.preparation.review'])} class="invoice-review" aria-live="polite">
|
|
16
|
+
<strong>{review.label}</strong>
|
|
17
|
+
{#if review.message}<p>{review.message}</p>{/if}
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<style>
|
|
21
|
+
.invoice-review { padding: var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); overflow-wrap: anywhere; }
|
|
22
|
+
p { margin-block: var(--smrt-spacing-2) 0; }
|
|
23
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InvoicePreparationReview } from '../invoices/types.js';
|
|
2
|
+
/** Caller-authoritative approval/review presentation, without mutation actions. */
|
|
3
|
+
export interface Props {
|
|
4
|
+
/** Server snapshot, including caller-localized unknown states. */
|
|
5
|
+
review: InvoicePreparationReview;
|
|
6
|
+
}
|
|
7
|
+
declare const InvoiceReview: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type InvoiceReview = ReturnType<typeof InvoiceReview>;
|
|
9
|
+
export default InvoiceReview;
|
|
10
|
+
//# sourceMappingURL=InvoiceReview.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InvoiceReview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/InvoiceReview.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAGrE,mFAAmF;AACnF,MAAM,WAAW,KAAK;IACpB,kEAAkE;IAClE,MAAM,EAAE,wBAAwB,CAAC;CAClC;AAmBD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CodeSelectOption } from '@happyvertical/smrt-ui/forms';
|
|
3
|
+
import {
|
|
4
|
+
CountrySelect,
|
|
5
|
+
FormGroup,
|
|
6
|
+
Input,
|
|
7
|
+
ProvinceSelect,
|
|
8
|
+
} from '@happyvertical/smrt-ui/forms';
|
|
9
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
10
|
+
import { M } from '../i18n.js';
|
|
11
|
+
import type { PartyAddressData } from '../party-types.js';
|
|
12
|
+
|
|
13
|
+
export interface AddressFieldNames {
|
|
14
|
+
street1: string;
|
|
15
|
+
street2: string;
|
|
16
|
+
city: string;
|
|
17
|
+
state: string;
|
|
18
|
+
postalCode: string;
|
|
19
|
+
country: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Props {
|
|
23
|
+
heading: string;
|
|
24
|
+
value?: PartyAddressData;
|
|
25
|
+
names: AddressFieldNames;
|
|
26
|
+
errors?: Record<string, string | undefined>;
|
|
27
|
+
readOnly?: boolean;
|
|
28
|
+
/** Caller-restricted country choices; omitted uses the shared ISO defaults. */
|
|
29
|
+
countryOptions?: readonly CodeSelectOption[];
|
|
30
|
+
/** Caller-restricted region choices; omitted uses CA/US defaults and text elsewhere. */
|
|
31
|
+
provinceOptions?: readonly CodeSelectOption[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
heading,
|
|
36
|
+
value = {},
|
|
37
|
+
names,
|
|
38
|
+
errors = {},
|
|
39
|
+
readOnly = false,
|
|
40
|
+
countryOptions,
|
|
41
|
+
provinceOptions,
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
const i18n = useI18n();
|
|
44
|
+
const { t } = i18n;
|
|
45
|
+
function addressValue(key: 'country' | 'state'): string {
|
|
46
|
+
return value[key] ?? '';
|
|
47
|
+
}
|
|
48
|
+
let country = $state(addressValue('country'));
|
|
49
|
+
let sourceCountry = $state(addressValue('country'));
|
|
50
|
+
let province = $state(addressValue('state'));
|
|
51
|
+
let sourceProvince = $state(addressValue('state'));
|
|
52
|
+
$effect(() => {
|
|
53
|
+
const nextCountry = value.country ?? '';
|
|
54
|
+
if (nextCountry !== sourceCountry) {
|
|
55
|
+
sourceCountry = nextCountry;
|
|
56
|
+
country = nextCountry;
|
|
57
|
+
}
|
|
58
|
+
const nextProvince = value.state ?? '';
|
|
59
|
+
if (nextProvince !== sourceProvince) {
|
|
60
|
+
sourceProvince = nextProvince;
|
|
61
|
+
province = nextProvince;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<fieldset class="address-fields">
|
|
67
|
+
<legend>{heading}</legend>
|
|
68
|
+
<FormGroup label={t(M['commerce.party.street_address'])} error={errors.street1}>
|
|
69
|
+
<Input name={names.street1} value={value.street1 ?? ''} maxlength={160} readonly={readOnly} />
|
|
70
|
+
</FormGroup>
|
|
71
|
+
<FormGroup label={t(M['commerce.party.address_line_2'])} error={errors.street2}>
|
|
72
|
+
<Input name={names.street2} value={value.street2 ?? ''} maxlength={160} readonly={readOnly} />
|
|
73
|
+
</FormGroup>
|
|
74
|
+
<div class="address-grid">
|
|
75
|
+
<FormGroup label={t(M['commerce.party.city'])} error={errors.city}>
|
|
76
|
+
<Input name={names.city} value={value.city ?? ''} maxlength={100} readonly={readOnly} />
|
|
77
|
+
</FormGroup>
|
|
78
|
+
<FormGroup label={t(M['commerce.party.country'])} error={errors.country}>
|
|
79
|
+
<CountrySelect name={names.country} bind:value={country} options={countryOptions} locale={i18n.locale} readOnly={readOnly} />
|
|
80
|
+
</FormGroup>
|
|
81
|
+
<FormGroup label={t(M['commerce.party.state'])} error={errors.state}>
|
|
82
|
+
<ProvinceSelect name={names.state} bind:value={province} {country} options={provinceOptions} locale={i18n.locale} readOnly={readOnly} />
|
|
83
|
+
</FormGroup>
|
|
84
|
+
<FormGroup label={t(M['commerce.party.postal_code'])} error={errors.postalCode}>
|
|
85
|
+
<Input name={names.postalCode} value={value.postalCode ?? ''} maxlength={32} readonly={readOnly} />
|
|
86
|
+
</FormGroup>
|
|
87
|
+
</div>
|
|
88
|
+
</fieldset>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
.address-fields { 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); }
|
|
92
|
+
legend { padding-inline: var(--smrt-spacing-2); font: var(--smrt-typography-title-medium-font); color: var(--smrt-color-on-surface); }
|
|
93
|
+
.address-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--smrt-spacing-4); }
|
|
94
|
+
@media (max-width: 30rem) { .address-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CodeSelectOption } from '@happyvertical/smrt-ui/forms';
|
|
2
|
+
import type { PartyAddressData } from '../party-types.js';
|
|
3
|
+
export interface AddressFieldNames {
|
|
4
|
+
street1: string;
|
|
5
|
+
street2: string;
|
|
6
|
+
city: string;
|
|
7
|
+
state: string;
|
|
8
|
+
postalCode: string;
|
|
9
|
+
country: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Props {
|
|
12
|
+
heading: string;
|
|
13
|
+
value?: PartyAddressData;
|
|
14
|
+
names: AddressFieldNames;
|
|
15
|
+
errors?: Record<string, string | undefined>;
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
/** Caller-restricted country choices; omitted uses the shared ISO defaults. */
|
|
18
|
+
countryOptions?: readonly CodeSelectOption[];
|
|
19
|
+
/** Caller-restricted region choices; omitted uses CA/US defaults and text elsewhere. */
|
|
20
|
+
provinceOptions?: readonly CodeSelectOption[];
|
|
21
|
+
}
|
|
22
|
+
declare const PartyAddressFields: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type PartyAddressFields = ReturnType<typeof PartyAddressFields>;
|
|
24
|
+
export default PartyAddressFields;
|
|
25
|
+
//# sourceMappingURL=PartyAddressFields.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PartyAddressFields.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/PartyAddressFields.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AASrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG1D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,wFAAwF;IACxF,eAAe,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AAsED,QAAA,MAAM,kBAAkB,2CAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { FormGroup, Input, Textarea } from '@happyvertical/smrt-ui/forms';
|
|
3
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
4
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
5
|
+
import { M } from '../i18n.js';
|
|
6
|
+
import type {
|
|
7
|
+
PartyContactData,
|
|
8
|
+
PartyContactLabels,
|
|
9
|
+
PartyFieldNames,
|
|
10
|
+
} from '../party-types.js';
|
|
11
|
+
import { DEFAULT_PARTY_FIELD_NAMES } from '../party-types.js';
|
|
12
|
+
|
|
13
|
+
/** Props for the provider-free repeatable contact composition. */
|
|
14
|
+
export interface Props {
|
|
15
|
+
/** Contact rows to display or submit in their given order. */
|
|
16
|
+
contacts?: PartyContactData[];
|
|
17
|
+
/** HTML field-name overrides used to preserve a caller's payload contract. */
|
|
18
|
+
fieldNames?: Partial<PartyFieldNames>;
|
|
19
|
+
/** Row-level validation errors returned by the server. */
|
|
20
|
+
errors?: Array<Record<string, string | undefined> | undefined>;
|
|
21
|
+
/** User-facing copy overrides for contact terminology. */
|
|
22
|
+
labels?: PartyContactLabels;
|
|
23
|
+
/** Renders contact values without editable controls or mutation actions. */
|
|
24
|
+
readOnly?: boolean;
|
|
25
|
+
/** Shows the native add-row submitter when editing. */
|
|
26
|
+
canAdd?: boolean;
|
|
27
|
+
/** Shows a native remove-row submitter for each contact when editing. */
|
|
28
|
+
canRemove?: boolean;
|
|
29
|
+
/** Name of the native submitter field used for row actions. */
|
|
30
|
+
intentName?: string;
|
|
31
|
+
/** Submitter value that asks the caller to append a row. */
|
|
32
|
+
addIntent?: string;
|
|
33
|
+
/** Produces the submitter value that asks the caller to remove one row. */
|
|
34
|
+
removeIntent?: (index: number) => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
contacts = [],
|
|
39
|
+
fieldNames = {},
|
|
40
|
+
errors = [],
|
|
41
|
+
labels = {},
|
|
42
|
+
readOnly = false,
|
|
43
|
+
canAdd = true,
|
|
44
|
+
canRemove = true,
|
|
45
|
+
intentName = 'intent',
|
|
46
|
+
addIntent = 'addContact',
|
|
47
|
+
removeIntent = (index: number) => `removeContact:${index}`,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
const { t } = useI18n();
|
|
51
|
+
const instanceId = $props.id();
|
|
52
|
+
const headingId = `party-contacts-heading-${instanceId}`;
|
|
53
|
+
const names = $derived({ ...DEFAULT_PARTY_FIELD_NAMES, ...fieldNames });
|
|
54
|
+
const text = $derived({
|
|
55
|
+
heading: labels.heading ?? t(M['commerce.party.contacts']),
|
|
56
|
+
empty: labels.empty ?? t(M['commerce.party.no_contacts']),
|
|
57
|
+
contact: labels.contact ?? t(M['commerce.party.contact']),
|
|
58
|
+
name: labels.name ?? t(M['commerce.party.contact_name']),
|
|
59
|
+
role: labels.role ?? t(M['commerce.party.contact_role']),
|
|
60
|
+
email: labels.email ?? t(M['commerce.party.contact_email']),
|
|
61
|
+
phone: labels.phone ?? t(M['commerce.party.contact_phone']),
|
|
62
|
+
address: labels.address ?? t(M['commerce.party.contact_address']),
|
|
63
|
+
add: labels.add ?? t(M['commerce.party.add_contact']),
|
|
64
|
+
remove: labels.remove ?? t(M['commerce.party.remove_contact']),
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<section class="contacts" aria-labelledby={headingId}>
|
|
69
|
+
<div class="contacts-heading">
|
|
70
|
+
<h2 id={headingId}>{text.heading}</h2>
|
|
71
|
+
{#if !readOnly && canAdd}
|
|
72
|
+
<Button
|
|
73
|
+
type="submit"
|
|
74
|
+
variant="secondary"
|
|
75
|
+
name={intentName}
|
|
76
|
+
value={addIntent}
|
|
77
|
+
formnovalidate
|
|
78
|
+
>{text.add}</Button>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{#if contacts.length === 0}
|
|
83
|
+
<p class="empty">{text.empty}</p>
|
|
84
|
+
{:else}
|
|
85
|
+
<div class="contact-list">
|
|
86
|
+
{#each contacts as contact, index (`${contact.id ?? 'new'}-${index}`)}
|
|
87
|
+
{#if readOnly}
|
|
88
|
+
<article class="contact-card">
|
|
89
|
+
<h3>{contact.name || `${text.contact} ${index + 1}`}</h3>
|
|
90
|
+
{#if contact.label}<p>{contact.label}</p>{/if}
|
|
91
|
+
{#if contact.email}<p><a href={`mailto:${contact.email}`}>{contact.email}</a></p>{/if}
|
|
92
|
+
{#if contact.phone}<p><a href={`tel:${contact.phone}`}>{contact.phone}</a></p>{/if}
|
|
93
|
+
{#if contact.address}<p class="address">{contact.address}</p>{/if}
|
|
94
|
+
</article>
|
|
95
|
+
{:else}
|
|
96
|
+
<fieldset class="contact-card">
|
|
97
|
+
<legend>{text.contact} {index + 1}</legend>
|
|
98
|
+
<Input interaction={false} type="hidden" name={names.contactId} value={contact.id ?? ''} />
|
|
99
|
+
<div class="field-grid">
|
|
100
|
+
<FormGroup label={text.name} error={errors[index]?.name}>
|
|
101
|
+
<Input name={names.contactName} value={contact.name ?? ''} maxlength={160} />
|
|
102
|
+
</FormGroup>
|
|
103
|
+
<FormGroup label={text.role} error={errors[index]?.label}>
|
|
104
|
+
<Input name={names.contactLabel} value={contact.label ?? ''} maxlength={80} />
|
|
105
|
+
</FormGroup>
|
|
106
|
+
<FormGroup label={text.email} error={errors[index]?.email}>
|
|
107
|
+
<Input name={names.contactEmail} type="email" value={contact.email ?? ''} maxlength={320} />
|
|
108
|
+
</FormGroup>
|
|
109
|
+
<FormGroup label={text.phone} error={errors[index]?.phone}>
|
|
110
|
+
<Input name={names.contactPhone} type="tel" value={contact.phone ?? ''} maxlength={80} />
|
|
111
|
+
</FormGroup>
|
|
112
|
+
</div>
|
|
113
|
+
<FormGroup label={text.address} error={errors[index]?.address}>
|
|
114
|
+
<Textarea name={names.contactAddress} value={contact.address ?? ''} rows={3} maxlength={500} />
|
|
115
|
+
</FormGroup>
|
|
116
|
+
{#if canRemove}
|
|
117
|
+
<div class="contact-actions">
|
|
118
|
+
<Button
|
|
119
|
+
type="submit"
|
|
120
|
+
variant="ghost"
|
|
121
|
+
name={intentName}
|
|
122
|
+
value={removeIntent(index)}
|
|
123
|
+
formnovalidate
|
|
124
|
+
>{text.remove}</Button>
|
|
125
|
+
</div>
|
|
126
|
+
{/if}
|
|
127
|
+
</fieldset>
|
|
128
|
+
{/if}
|
|
129
|
+
{/each}
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
</section>
|
|
133
|
+
|
|
134
|
+
<style>
|
|
135
|
+
.contacts { display: grid; gap: var(--smrt-spacing-4); min-inline-size: 0; }
|
|
136
|
+
.contacts-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--smrt-spacing-3); flex-wrap: wrap; }
|
|
137
|
+
h2, h3, p { margin: 0; }
|
|
138
|
+
h2 { font: var(--smrt-typography-title-large-font); color: var(--smrt-color-on-surface); }
|
|
139
|
+
h3, legend { font: var(--smrt-typography-title-medium-font); color: var(--smrt-color-on-surface); }
|
|
140
|
+
.contact-list { display: grid; gap: var(--smrt-spacing-4); }
|
|
141
|
+
.contact-card { 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); background: var(--smrt-color-surface-container-low); overflow-wrap: anywhere; }
|
|
142
|
+
article.contact-card { display: grid; gap: var(--smrt-spacing-2); }
|
|
143
|
+
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--smrt-spacing-4); }
|
|
144
|
+
.field-grid :global(.form-group) { min-inline-size: 0; }
|
|
145
|
+
.contact-actions { display: flex; justify-content: flex-end; }
|
|
146
|
+
.empty { color: var(--smrt-color-on-surface-variant); }
|
|
147
|
+
.address { white-space: pre-wrap; }
|
|
148
|
+
a { color: var(--smrt-color-primary); }
|
|
149
|
+
@media (max-width: 30rem) {
|
|
150
|
+
.field-grid { grid-template-columns: minmax(0, 1fr); }
|
|
151
|
+
.contact-card { padding: var(--smrt-spacing-3); }
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PartyContactData, PartyContactLabels, PartyFieldNames } from '../party-types.js';
|
|
2
|
+
/** Props for the provider-free repeatable contact composition. */
|
|
3
|
+
export interface Props {
|
|
4
|
+
/** Contact rows to display or submit in their given order. */
|
|
5
|
+
contacts?: PartyContactData[];
|
|
6
|
+
/** HTML field-name overrides used to preserve a caller's payload contract. */
|
|
7
|
+
fieldNames?: Partial<PartyFieldNames>;
|
|
8
|
+
/** Row-level validation errors returned by the server. */
|
|
9
|
+
errors?: Array<Record<string, string | undefined> | undefined>;
|
|
10
|
+
/** User-facing copy overrides for contact terminology. */
|
|
11
|
+
labels?: PartyContactLabels;
|
|
12
|
+
/** Renders contact values without editable controls or mutation actions. */
|
|
13
|
+
readOnly?: boolean;
|
|
14
|
+
/** Shows the native add-row submitter when editing. */
|
|
15
|
+
canAdd?: boolean;
|
|
16
|
+
/** Shows a native remove-row submitter for each contact when editing. */
|
|
17
|
+
canRemove?: boolean;
|
|
18
|
+
/** Name of the native submitter field used for row actions. */
|
|
19
|
+
intentName?: string;
|
|
20
|
+
/** Submitter value that asks the caller to append a row. */
|
|
21
|
+
addIntent?: string;
|
|
22
|
+
/** Produces the submitter value that asks the caller to remove one row. */
|
|
23
|
+
removeIntent?: (index: number) => string;
|
|
24
|
+
}
|
|
25
|
+
declare const PartyContactFields: import("svelte").Component<Props, {}, "">;
|
|
26
|
+
type PartyContactFields = ReturnType<typeof PartyContactFields>;
|
|
27
|
+
export default PartyContactFields;
|
|
28
|
+
//# sourceMappingURL=PartyContactFields.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PartyContactFields.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/PartyContactFields.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAI3B,kEAAkE;AAClE,MAAM,WAAW,KAAK;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC9B,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACtC,0DAA0D;IAC1D,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC;IAC/D,0DAA0D;IAC1D,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAC1C;AAmGD,QAAA,MAAM,kBAAkB,2CAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
|