@happyvertical/smrt-commerce 0.53.3 → 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,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type QuoteRevision } from '../quote-types.js';
|
|
3
|
+
/** Read-only revision evidence, in caller-provided order. */
|
|
4
|
+
export interface Props {
|
|
5
|
+
/** Immutable summaries, including unknown caller status labels. */
|
|
6
|
+
revisions: QuoteRevision[];
|
|
7
|
+
/** Optional localized heading. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Optional authorized evidence/details for each revision. */
|
|
10
|
+
evidence?: Snippet<[QuoteRevision]>;
|
|
11
|
+
}
|
|
12
|
+
declare const QuoteRevisionHistory: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type QuoteRevisionHistory = ReturnType<typeof QuoteRevisionHistory>;
|
|
14
|
+
export default QuoteRevisionHistory;
|
|
15
|
+
//# sourceMappingURL=QuoteRevisionHistory.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuoteRevisionHistory.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/QuoteRevisionHistory.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,mBAAmB,CAAC;AAE3B,6DAA6D;AAC7D,MAAM,WAAW,KAAK;IACpB,mEAAmE;IACnE,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;CACrC;AA0CD,QAAA,MAAM,oBAAoB,2CAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { StatusBadge } from '@happyvertical/smrt-ui';
|
|
3
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
4
|
+
import { PageHeader } from '@happyvertical/smrt-ui/layout';
|
|
5
|
+
import { Button, Card } from '@happyvertical/smrt-ui/ui';
|
|
6
|
+
import { M } from '../i18n.js';
|
|
7
|
+
import type {
|
|
8
|
+
PartyContactLabels,
|
|
9
|
+
PartyExtension,
|
|
10
|
+
PartySurfaceLabels,
|
|
11
|
+
VendorDisplayData,
|
|
12
|
+
} from '../party-types.js';
|
|
13
|
+
import { formatPartyMinorUnits } from '../party-types.js';
|
|
14
|
+
import PartyContactFields from './PartyContactFields.svelte';
|
|
15
|
+
|
|
16
|
+
/** Props for the vendor detail/summary surface. */
|
|
17
|
+
export interface Props {
|
|
18
|
+
/** Vendor and Profile projection to render. */
|
|
19
|
+
vendor: VendorDisplayData;
|
|
20
|
+
/** Vendor terminology and action-label overrides. */
|
|
21
|
+
labels?: PartySurfaceLabels;
|
|
22
|
+
/** Contact terminology overrides. */
|
|
23
|
+
contactLabels?: PartyContactLabels;
|
|
24
|
+
/** Caller-owned URL back to the vendor directory. */
|
|
25
|
+
backHref?: string;
|
|
26
|
+
/** Caller-owned URL for editing this vendor. */
|
|
27
|
+
editHref?: string;
|
|
28
|
+
/** Controls whether the edit affordance is presented. */
|
|
29
|
+
canEdit?: boolean;
|
|
30
|
+
/** Currency exponent used only for minor-unit display conversion. */
|
|
31
|
+
minorUnitExponent?: number;
|
|
32
|
+
/** Locale used for currency display. */
|
|
33
|
+
locale?: string;
|
|
34
|
+
/** Consumer content rendered after the standard vendor sections. */
|
|
35
|
+
extension?: PartyExtension<VendorDisplayData>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
vendor,
|
|
40
|
+
labels = {},
|
|
41
|
+
contactLabels,
|
|
42
|
+
backHref,
|
|
43
|
+
editHref,
|
|
44
|
+
canEdit = false,
|
|
45
|
+
minorUnitExponent = 2,
|
|
46
|
+
locale = 'en',
|
|
47
|
+
extension,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
const { t } = useI18n();
|
|
50
|
+
const singular = $derived(labels.singular ?? t(M['commerce.vendor.singular']));
|
|
51
|
+
const plural = $derived(labels.plural ?? t(M['commerce.vendor.plural']));
|
|
52
|
+
const notRecorded = $derived(t(M['commerce.party.not_recorded']));
|
|
53
|
+
const amountUnavailable = $derived(t(M['commerce.party.amount_unavailable']));
|
|
54
|
+
|
|
55
|
+
function statusLabel(value: string): string {
|
|
56
|
+
if (value === 'active') return t(M['commerce.party.status_active']);
|
|
57
|
+
if (value === 'inactive') return t(M['commerce.party.status_inactive']);
|
|
58
|
+
if (value === 'suspended') return t(M['commerce.party.status_suspended']);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function identityKindLabel(value: string | undefined): string {
|
|
63
|
+
if (value === 'business') return t(M['commerce.party.identity_business']);
|
|
64
|
+
if (value === 'person') return t(M['commerce.party.identity_person']);
|
|
65
|
+
return value || notRecorded;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function minimumOrder(amountMinor: number): string {
|
|
69
|
+
try {
|
|
70
|
+
return formatPartyMinorUnits(
|
|
71
|
+
amountMinor,
|
|
72
|
+
vendor.currency ?? 'USD',
|
|
73
|
+
minorUnitExponent,
|
|
74
|
+
locale,
|
|
75
|
+
);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof RangeError) return amountUnavailable;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<section class="party-detail">
|
|
84
|
+
{#if backHref}<Button href={backHref} variant="ghost">{t(M['commerce.party.back_to'], { plural })}</Button>{/if}
|
|
85
|
+
<PageHeader title={vendor.profile.name} subtitle={vendor.profile.description ?? t(M['commerce.vendor.details'])}>
|
|
86
|
+
{#snippet actions()}
|
|
87
|
+
{#if canEdit && editHref}<Button href={editHref}>{labels.edit ?? (labels.singular ? t(M['commerce.party.edit_named'], { singular }) : t(M['commerce.vendor.edit']))}</Button>{/if}
|
|
88
|
+
{/snippet}
|
|
89
|
+
</PageHeader>
|
|
90
|
+
|
|
91
|
+
<div class="summary-grid">
|
|
92
|
+
<Card padding="md">
|
|
93
|
+
<h2>{t(M['commerce.party.identity'])}</h2>
|
|
94
|
+
<dl>
|
|
95
|
+
<dt>{t(M['commerce.party.type'])}</dt><dd>{identityKindLabel(vendor.profile.identityKind)}</dd>
|
|
96
|
+
<dt>{t(M['commerce.party.status'])}</dt><dd><StatusBadge status={vendor.status} label={statusLabel(vendor.status)} /></dd>
|
|
97
|
+
<dt>{t(M['commerce.party.email'])}</dt><dd>{#if vendor.profile.email}<a href={`mailto:${vendor.profile.email}`}>{vendor.profile.email}</a>{:else}{notRecorded}{/if}</dd>
|
|
98
|
+
</dl>
|
|
99
|
+
</Card>
|
|
100
|
+
<Card padding="md">
|
|
101
|
+
<h2>{t(M['commerce.party.commercial_terms'])}</h2>
|
|
102
|
+
<dl>
|
|
103
|
+
<dt>{t(M['commerce.vendor.lead_time'])}</dt><dd>{vendor.leadTimeDays === undefined ? notRecorded : t(M['commerce.vendor.lead_time_days'], { days: vendor.leadTimeDays })}</dd>
|
|
104
|
+
<dt>{t(M['commerce.vendor.minimum_order'])}</dt><dd>{vendor.minimumOrderMinor === undefined ? notRecorded : minimumOrder(vendor.minimumOrderMinor)}</dd>
|
|
105
|
+
<dt>{t(M['commerce.party.payment_terms'])}</dt><dd>{vendor.paymentTerms || notRecorded}</dd>
|
|
106
|
+
<dt>{t(M['commerce.vendor.currency'])}</dt><dd>{vendor.currency || notRecorded}</dd>
|
|
107
|
+
</dl>
|
|
108
|
+
</Card>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<Card padding="md">
|
|
112
|
+
<h2>{t(M['commerce.vendor.default_order_contact'])}</h2>
|
|
113
|
+
<dl>
|
|
114
|
+
<dt>{t(M['commerce.party.email'])}</dt><dd>{#if vendor.defaultContactEmail}<a href={`mailto:${vendor.defaultContactEmail}`}>{vendor.defaultContactEmail}</a>{:else}{notRecorded}{/if}</dd>
|
|
115
|
+
<dt>{t(M['commerce.party.phone'])}</dt><dd>{#if vendor.defaultContactPhone}<a href={`tel:${vendor.defaultContactPhone}`}>{vendor.defaultContactPhone}</a>{:else}{notRecorded}{/if}</dd>
|
|
116
|
+
</dl>
|
|
117
|
+
</Card>
|
|
118
|
+
|
|
119
|
+
<PartyContactFields contacts={vendor.contacts ?? []} labels={contactLabels} readOnly />
|
|
120
|
+
{#if vendor.notes}<Card padding="md"><h2>{t(M['commerce.party.notes'])}</h2><p class="notes">{vendor.notes}</p></Card>{/if}
|
|
121
|
+
{@render extension?.(vendor)}
|
|
122
|
+
</section>
|
|
123
|
+
|
|
124
|
+
<style>
|
|
125
|
+
.party-detail { display: grid; gap: var(--smrt-spacing-5); max-inline-size: 64rem; min-inline-size: 0; overflow-wrap: anywhere; }
|
|
126
|
+
.summary-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--smrt-spacing-4); }
|
|
127
|
+
h2 { margin: 0 0 var(--smrt-spacing-4); font: var(--smrt-typography-title-medium-font); color: var(--smrt-color-on-surface); }
|
|
128
|
+
dl { display: grid; grid-template-columns: minmax(7rem, max-content) minmax(0, 1fr); gap: var(--smrt-spacing-2) var(--smrt-spacing-4); margin: 0; }
|
|
129
|
+
dt { color: var(--smrt-color-on-surface-variant); }
|
|
130
|
+
dd { margin: 0; }
|
|
131
|
+
a { color: var(--smrt-color-primary); }
|
|
132
|
+
.notes { margin: 0; white-space: pre-wrap; }
|
|
133
|
+
@media (max-width: 42rem) { .summary-grid { grid-template-columns: minmax(0, 1fr); } dl { grid-template-columns: minmax(0, 1fr); gap: var(--smrt-spacing-1); } dd { margin-block-end: var(--smrt-spacing-2); } }
|
|
134
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PartyContactLabels, PartyExtension, PartySurfaceLabels, VendorDisplayData } from '../party-types.js';
|
|
2
|
+
/** Props for the vendor detail/summary surface. */
|
|
3
|
+
export interface Props {
|
|
4
|
+
/** Vendor and Profile projection to render. */
|
|
5
|
+
vendor: VendorDisplayData;
|
|
6
|
+
/** Vendor terminology and action-label overrides. */
|
|
7
|
+
labels?: PartySurfaceLabels;
|
|
8
|
+
/** Contact terminology overrides. */
|
|
9
|
+
contactLabels?: PartyContactLabels;
|
|
10
|
+
/** Caller-owned URL back to the vendor directory. */
|
|
11
|
+
backHref?: string;
|
|
12
|
+
/** Caller-owned URL for editing this vendor. */
|
|
13
|
+
editHref?: string;
|
|
14
|
+
/** Controls whether the edit affordance is presented. */
|
|
15
|
+
canEdit?: boolean;
|
|
16
|
+
/** Currency exponent used only for minor-unit display conversion. */
|
|
17
|
+
minorUnitExponent?: number;
|
|
18
|
+
/** Locale used for currency display. */
|
|
19
|
+
locale?: string;
|
|
20
|
+
/** Consumer content rendered after the standard vendor sections. */
|
|
21
|
+
extension?: PartyExtension<VendorDisplayData>;
|
|
22
|
+
}
|
|
23
|
+
declare const VendorDetail: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type VendorDetail = ReturnType<typeof VendorDetail>;
|
|
25
|
+
export default VendorDetail;
|
|
26
|
+
//# sourceMappingURL=VendorDetail.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VendorDetail.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/VendorDetail.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AAK3B,mDAAmD;AACnD,MAAM,WAAW,KAAK;IACpB,+CAA+C;IAC/C,MAAM,EAAE,iBAAiB,CAAC;IAC1B,qDAAqD;IACrD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,qCAAqC;IACrC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,SAAS,CAAC,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAC;CAC/C;AAsGD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
PartyDirectoryExtension,
|
|
11
|
+
PartyDirectoryItem,
|
|
12
|
+
PartySurfaceLabels,
|
|
13
|
+
VendorDisplayData,
|
|
14
|
+
} from '../party-types.js';
|
|
15
|
+
|
|
16
|
+
/** Props for the searchable, paginated vendor directory. */
|
|
17
|
+
export interface Props {
|
|
18
|
+
/** Vendor rows and caller-owned detail links. */
|
|
19
|
+
items?: PartyDirectoryItem<VendorDisplayData>[];
|
|
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 vendor. */
|
|
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
|
+
/** Vendor terminology and action-label overrides. */
|
|
49
|
+
labels?: PartySurfaceLabels;
|
|
50
|
+
/** Consumer content appended to every vendor row. */
|
|
51
|
+
extension?: PartyDirectoryExtension<VendorDisplayData>;
|
|
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(labels.singular ?? t(M['commerce.vendor.singular']));
|
|
76
|
+
const plural = $derived(labels.plural ?? t(M['commerce.vendor.plural']));
|
|
77
|
+
const first = $derived(
|
|
78
|
+
pageSize === undefined || total === 0 ? 0 : (page - 1) * pageSize + 1,
|
|
79
|
+
);
|
|
80
|
+
const last = $derived(
|
|
81
|
+
pageSize === undefined ? 0 : Math.min(page * pageSize, total),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
function statusLabel(value: string): string {
|
|
85
|
+
if (value === 'active') return t(M['commerce.party.status_active']);
|
|
86
|
+
if (value === 'inactive') return t(M['commerce.party.status_inactive']);
|
|
87
|
+
if (value === 'suspended') return t(M['commerce.party.status_suspended']);
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function vendorMeta(vendor: VendorDisplayData): string {
|
|
92
|
+
const leadTime =
|
|
93
|
+
vendor.leadTimeDays === undefined
|
|
94
|
+
? t(M['commerce.vendor.lead_time_missing'])
|
|
95
|
+
: t(M['commerce.vendor.lead_time_days'], { days: vendor.leadTimeDays });
|
|
96
|
+
return vendor.paymentTerms
|
|
97
|
+
? `${leadTime} · ${vendor.paymentTerms}`
|
|
98
|
+
: leadTime;
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<section class="party-directory">
|
|
103
|
+
<PageHeader title={plural} subtitle={labels.plural ? t(M['commerce.party.manage_named'], { plural }) : t(M['commerce.vendor.manage'])}>
|
|
104
|
+
{#snippet actions()}
|
|
105
|
+
{#if canCreate && addHref}<Button href={addHref}>{labels.add ?? (labels.singular ? t(M['commerce.party.add_named'], { singular }) : t(M['commerce.vendor.add']))}</Button>{/if}
|
|
106
|
+
{/snippet}
|
|
107
|
+
</PageHeader>
|
|
108
|
+
|
|
109
|
+
<Form class="directory-search" method="get" action={searchAction} preventDefault={false} stagedReview={false}>
|
|
110
|
+
<FormGroup label={labels.search ?? (labels.plural ? t(M['commerce.party.search_named'], { plural }) : t(M['commerce.vendor.search']))}>
|
|
111
|
+
<Input type="search" name={queryName} value={query} maxlength={160} />
|
|
112
|
+
</FormGroup>
|
|
113
|
+
<FormGroup label={t(M['commerce.party.status'])}>
|
|
114
|
+
<Select name={statusName} value={status}>
|
|
115
|
+
<option value="">{t(M['commerce.party.status_all'])}</option>
|
|
116
|
+
{#if status && !['active', 'inactive', 'suspended'].includes(status)}
|
|
117
|
+
<option value={status}>{status}</option>
|
|
118
|
+
{/if}
|
|
119
|
+
<option value="active">{t(M['commerce.party.status_active'])}</option>
|
|
120
|
+
<option value="inactive">{t(M['commerce.party.status_inactive'])}</option>
|
|
121
|
+
<option value="suspended">{t(M['commerce.party.status_suspended'])}</option>
|
|
122
|
+
</Select>
|
|
123
|
+
</FormGroup>
|
|
124
|
+
<Button type="submit" variant="secondary">{t(M['commerce.party.search'])}</Button>
|
|
125
|
+
</Form>
|
|
126
|
+
|
|
127
|
+
{#if errorMessage}
|
|
128
|
+
<Alert variant="error" title={t(M['commerce.vendor.unable_to_load'])}>{errorMessage}</Alert>
|
|
129
|
+
{:else if loading}
|
|
130
|
+
<div class="loading" role="status" aria-label={labels.loading ?? t(M['commerce.vendor.loading'])}>
|
|
131
|
+
<Skeleton height="6rem" /><Skeleton height="6rem" /><Skeleton height="6rem" />
|
|
132
|
+
</div>
|
|
133
|
+
{:else if items.length === 0}
|
|
134
|
+
<EmptyState title={labels.empty ?? t(M['commerce.vendor.no_results'])} description={t(M['commerce.party.try_filters'])} />
|
|
135
|
+
{:else}
|
|
136
|
+
<p class="result-count">
|
|
137
|
+
{pageSize === undefined
|
|
138
|
+
? t(M['commerce.party.showing_count'], { count: items.length, total })
|
|
139
|
+
: t(M['commerce.party.showing'], { first, last, total })}
|
|
140
|
+
</p>
|
|
141
|
+
<div class="party-list">
|
|
142
|
+
{#each items as item (item.data.id)}
|
|
143
|
+
<Card padding="md" hoverable data-party-id={item.data.id}>
|
|
144
|
+
<div class="party-row">
|
|
145
|
+
<div class="party-copy">
|
|
146
|
+
<h2>{#if item.href}<a href={item.href}>{item.data.profile.name}</a>{:else}{item.data.profile.name}{/if}</h2>
|
|
147
|
+
<p>{item.data.defaultContactEmail || item.data.profile.email || t(M['commerce.party.no_contact_details'])}</p>
|
|
148
|
+
<p class="meta">{vendorMeta(item.data)}</p>
|
|
149
|
+
</div>
|
|
150
|
+
<StatusBadge status={item.data.status} label={statusLabel(item.data.status)} />
|
|
151
|
+
</div>
|
|
152
|
+
{@render extension?.(item.data)}
|
|
153
|
+
</Card>
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
<nav class="pagination" aria-label={t(M['commerce.party.pagination'], { singular })}>
|
|
157
|
+
{#if previousHref}<Button href={previousHref} variant="secondary">{labels.previous ?? t(M['commerce.party.previous'])}</Button>{/if}
|
|
158
|
+
<span>{t(M['commerce.party.page'], { page })}</span>
|
|
159
|
+
{#if nextHref}<Button href={nextHref} variant="secondary">{labels.next ?? t(M['commerce.party.next'])}</Button>{/if}
|
|
160
|
+
</nav>
|
|
161
|
+
{/if}
|
|
162
|
+
</section>
|
|
163
|
+
|
|
164
|
+
<style>
|
|
165
|
+
.party-directory { display: grid; gap: var(--smrt-spacing-5); max-inline-size: 64rem; min-inline-size: 0; }
|
|
166
|
+
:global(.directory-search) { display: grid; grid-template-columns: minmax(12rem, 2fr) minmax(10rem, 1fr) auto; align-items: end; gap: var(--smrt-spacing-3); }
|
|
167
|
+
:global(.directory-search .form-group) { margin: 0; min-inline-size: 0; }
|
|
168
|
+
.loading, .party-list { display: grid; gap: var(--smrt-spacing-3); }
|
|
169
|
+
.result-count { margin: 0; color: var(--smrt-color-on-surface-variant); }
|
|
170
|
+
.party-row { display: flex; align-items: start; justify-content: space-between; gap: var(--smrt-spacing-4); }
|
|
171
|
+
.party-copy { min-inline-size: 0; }
|
|
172
|
+
h2, p { margin: 0; }
|
|
173
|
+
h2 { font: var(--smrt-typography-title-medium-font); }
|
|
174
|
+
h2 a { color: var(--smrt-color-primary); text-decoration-thickness: 1px; text-underline-offset: 0.15em; }
|
|
175
|
+
.meta { margin-block-start: var(--smrt-spacing-2); color: var(--smrt-color-on-surface-variant); }
|
|
176
|
+
.pagination { display: flex; align-items: center; justify-content: center; gap: var(--smrt-spacing-3); }
|
|
177
|
+
@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; } }
|
|
178
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PartyDirectoryExtension, PartyDirectoryItem, PartySurfaceLabels, VendorDisplayData } from '../party-types.js';
|
|
2
|
+
/** Props for the searchable, paginated vendor directory. */
|
|
3
|
+
export interface Props {
|
|
4
|
+
/** Vendor rows and caller-owned detail links. */
|
|
5
|
+
items?: PartyDirectoryItem<VendorDisplayData>[];
|
|
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 vendor. */
|
|
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
|
+
/** Vendor terminology and action-label overrides. */
|
|
35
|
+
labels?: PartySurfaceLabels;
|
|
36
|
+
/** Consumer content appended to every vendor row. */
|
|
37
|
+
extension?: PartyDirectoryExtension<VendorDisplayData>;
|
|
38
|
+
}
|
|
39
|
+
declare const VendorDirectory: import("svelte").Component<Props, {}, "">;
|
|
40
|
+
type VendorDirectory = ReturnType<typeof VendorDirectory>;
|
|
41
|
+
export default VendorDirectory;
|
|
42
|
+
//# sourceMappingURL=VendorDirectory.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VendorDirectory.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/VendorDirectory.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AAG3B,4DAA4D;AAC5D,MAAM,WAAW,KAAK;IACpB,iDAAiD;IACjD,KAAK,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,EAAE,CAAC;IAChD,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,8CAA8C;IAC9C,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,qDAAqD;IACrD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,qDAAqD;IACrD,SAAS,CAAC,EAAE,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;CACxD;AA8HD,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CodeSelectOption } from '@happyvertical/smrt-ui/forms';
|
|
3
|
+
import {
|
|
4
|
+
CurrencySelect,
|
|
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
|
+
PartyContactLabels,
|
|
19
|
+
PartyExtension,
|
|
20
|
+
PartyFormErrors,
|
|
21
|
+
PartyFormTransport,
|
|
22
|
+
PartySurfaceLabels,
|
|
23
|
+
VendorFieldNames,
|
|
24
|
+
VendorFormValues,
|
|
25
|
+
} from '../party-types.js';
|
|
26
|
+
import { DEFAULT_VENDOR_FIELD_NAMES } from '../party-types.js';
|
|
27
|
+
import PartyContactFields from './PartyContactFields.svelte';
|
|
28
|
+
|
|
29
|
+
/** Props for the native, SSR-safe vendor create/edit form. */
|
|
30
|
+
export interface Props {
|
|
31
|
+
/** Selects create or edit copy and identity-lock defaults. */
|
|
32
|
+
mode?: 'create' | 'edit';
|
|
33
|
+
/** Exact vendor values to render, including server-retained strings. */
|
|
34
|
+
values?: VendorFormValues;
|
|
35
|
+
/** Caller-owned native form action, method, tokens, and submitter intents. */
|
|
36
|
+
transport?: PartyFormTransport;
|
|
37
|
+
/** HTML field-name overrides used to preserve an existing payload contract. */
|
|
38
|
+
fieldNames?: Partial<VendorFieldNames>;
|
|
39
|
+
/** Form, field, and contact errors returned by the server. */
|
|
40
|
+
errors?: PartyFormErrors;
|
|
41
|
+
/** Vendor terminology and action-label overrides. */
|
|
42
|
+
labels?: PartySurfaceLabels;
|
|
43
|
+
/** Contact terminology overrides. */
|
|
44
|
+
contactLabels?: PartyContactLabels;
|
|
45
|
+
/** Caller-restricted currency choices; omitted uses the shared ISO defaults. */
|
|
46
|
+
currencyOptions?: readonly CodeSelectOption[];
|
|
47
|
+
/** Renders values without mutation affordances. */
|
|
48
|
+
readOnly?: boolean;
|
|
49
|
+
/** Controls whether the save submitter is presented. */
|
|
50
|
+
canSubmit?: boolean;
|
|
51
|
+
/** Controls whether the native add-contact submitter is presented. */
|
|
52
|
+
canAddContact?: boolean;
|
|
53
|
+
/** Controls whether native remove-contact submitters are presented. */
|
|
54
|
+
canRemoveContact?: boolean;
|
|
55
|
+
/** Keeps an established Profile identity category from being converted. */
|
|
56
|
+
lockIdentityKind?: boolean;
|
|
57
|
+
/** Caller-owned URL used by the default cancel action. */
|
|
58
|
+
cancelHref?: string;
|
|
59
|
+
/** Consumer fields rendered before the action bar. */
|
|
60
|
+
extension?: PartyExtension<VendorFormValues>;
|
|
61
|
+
/** Replaces the default cancel and save action bar. */
|
|
62
|
+
actions?: Snippet;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const {
|
|
66
|
+
mode = 'create',
|
|
67
|
+
values = {},
|
|
68
|
+
transport = {},
|
|
69
|
+
fieldNames = {},
|
|
70
|
+
errors = {},
|
|
71
|
+
labels = {},
|
|
72
|
+
contactLabels,
|
|
73
|
+
currencyOptions,
|
|
74
|
+
readOnly = false,
|
|
75
|
+
canSubmit = true,
|
|
76
|
+
canAddContact = true,
|
|
77
|
+
canRemoveContact = true,
|
|
78
|
+
lockIdentityKind = mode === 'edit',
|
|
79
|
+
cancelHref,
|
|
80
|
+
extension,
|
|
81
|
+
actions,
|
|
82
|
+
}: Props = $props();
|
|
83
|
+
|
|
84
|
+
const i18n = useI18n();
|
|
85
|
+
const { t } = i18n;
|
|
86
|
+
const singular = $derived(labels.singular ?? t(M['commerce.vendor.singular']));
|
|
87
|
+
const names = $derived({ ...DEFAULT_VENDOR_FIELD_NAMES, ...fieldNames });
|
|
88
|
+
const intentName = $derived(transport.intentName ?? 'intent');
|
|
89
|
+
const errorList = $derived(
|
|
90
|
+
Object.entries(errors.fields ?? {})
|
|
91
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
92
|
+
.map(([field, message]) => ({
|
|
93
|
+
controlId:
|
|
94
|
+
field in names ? names[field as keyof VendorFieldNames] : field,
|
|
95
|
+
message,
|
|
96
|
+
})),
|
|
97
|
+
);
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<Form
|
|
101
|
+
class="party-form"
|
|
102
|
+
action={transport.action}
|
|
103
|
+
method={transport.method ?? 'post'}
|
|
104
|
+
preventDefault={false}
|
|
105
|
+
stagedReview={false}
|
|
106
|
+
>
|
|
107
|
+
{#each Object.entries(transport.hiddenFields ?? {}) as [name, value]}
|
|
108
|
+
<Input interaction={false} type="hidden" {name} {value} />
|
|
109
|
+
{/each}
|
|
110
|
+
{#if values.profileId}
|
|
111
|
+
<Input interaction={false} type="hidden" name={names.profileId} value={values.profileId} />
|
|
112
|
+
{/if}
|
|
113
|
+
{#if !readOnly && canSubmit}
|
|
114
|
+
<span class="native-default-submit" aria-hidden="true">
|
|
115
|
+
<Button type="submit" name={intentName} value={transport.saveIntent ?? 'save'} tabindex={-1}>
|
|
116
|
+
{labels.save ?? (labels.singular ? t(M[mode === 'create' ? 'commerce.party.add_named' : 'commerce.party.save_named'], { singular: labels.singular }) : t(M[mode === 'create' ? 'commerce.vendor.add' : 'commerce.vendor.save']))}
|
|
117
|
+
</Button>
|
|
118
|
+
</span>
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
{#if errors.form}<p class="form-error" role="alert">{errors.form}</p>{/if}
|
|
122
|
+
<ErrorSummary errors={errorList} />
|
|
123
|
+
|
|
124
|
+
<fieldset class="form-section">
|
|
125
|
+
<legend>{t(M['commerce.party.identity'])}</legend>
|
|
126
|
+
{#if lockIdentityKind && values.identityKind}
|
|
127
|
+
<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>
|
|
128
|
+
<Input interaction={false} type="hidden" name={names.identityKind} value={values.identityKind} />
|
|
129
|
+
{:else}
|
|
130
|
+
<FormGroup label={t(M['commerce.party.identity'])} required error={errors.fields?.identityKind}>
|
|
131
|
+
<Select name={names.identityKind} value={values.identityKind ?? ''} required disabled={readOnly}>
|
|
132
|
+
<option value="">{t(M['commerce.party.select_identity'])}</option>
|
|
133
|
+
<option value="business">{t(M['commerce.party.identity_business'])}</option>
|
|
134
|
+
<option value="person">{t(M['commerce.party.identity_person'])}</option>
|
|
135
|
+
</Select>
|
|
136
|
+
</FormGroup>
|
|
137
|
+
{/if}
|
|
138
|
+
<FormGroup label={t(M['commerce.party.name'], { singular })} required error={errors.fields?.name}>
|
|
139
|
+
<Input name={names.name} value={values.name ?? ''} maxlength={160} required readonly={readOnly} />
|
|
140
|
+
</FormGroup>
|
|
141
|
+
<div class="field-grid">
|
|
142
|
+
<FormGroup label={t(M['commerce.party.profile_email'])} error={errors.fields?.email}>
|
|
143
|
+
<Input name={names.email} type="email" value={values.email ?? ''} maxlength={320} readonly={readOnly} />
|
|
144
|
+
</FormGroup>
|
|
145
|
+
<FormGroup label={t(M['commerce.party.status'])} error={errors.fields?.status}>
|
|
146
|
+
<Select name={names.status} value={values.status ?? 'active'} disabled={readOnly}>
|
|
147
|
+
{#if values.status && !['active', 'inactive', 'suspended'].includes(values.status)}
|
|
148
|
+
<option value={values.status}>{values.status}</option>
|
|
149
|
+
{/if}
|
|
150
|
+
<option value="active">{t(M['commerce.party.status_active'])}</option>
|
|
151
|
+
<option value="inactive">{t(M['commerce.party.status_inactive'])}</option>
|
|
152
|
+
<option value="suspended">{t(M['commerce.party.status_suspended'])}</option>
|
|
153
|
+
</Select>
|
|
154
|
+
</FormGroup>
|
|
155
|
+
</div>
|
|
156
|
+
<FormGroup label={t(M['commerce.party.description'])} error={errors.fields?.description}>
|
|
157
|
+
<Textarea name={names.description} value={values.description ?? ''} rows={3} maxlength={1000} readonly={readOnly} />
|
|
158
|
+
</FormGroup>
|
|
159
|
+
</fieldset>
|
|
160
|
+
|
|
161
|
+
<fieldset class="form-section">
|
|
162
|
+
<legend>{t(M['commerce.party.commercial_terms'])}</legend>
|
|
163
|
+
<div class="field-grid">
|
|
164
|
+
<FormGroup label={t(M['commerce.vendor.lead_time'])} error={errors.fields?.leadTimeDays}>
|
|
165
|
+
<Input name={names.leadTimeDays} inputmode="numeric" value={values.leadTimeDays ?? ''} readonly={readOnly} />
|
|
166
|
+
</FormGroup>
|
|
167
|
+
<FormGroup label={t(M['commerce.vendor.minimum_order'])} hint={t(M['commerce.vendor.minimum_order_hint'])} error={errors.fields?.minimumOrder}>
|
|
168
|
+
<Input name={names.minimumOrder} inputmode="decimal" value={values.minimumOrder ?? ''} readonly={readOnly} />
|
|
169
|
+
</FormGroup>
|
|
170
|
+
<FormGroup label={t(M['commerce.vendor.currency'])} error={errors.fields?.currency}>
|
|
171
|
+
<CurrencySelect name={names.currency} value={values.currency ?? 'USD'} options={currencyOptions} locale={i18n.locale} readOnly={readOnly} />
|
|
172
|
+
</FormGroup>
|
|
173
|
+
<FormGroup label={t(M['commerce.party.payment_terms'])} error={errors.fields?.paymentTerms}>
|
|
174
|
+
<Input name={names.paymentTerms} value={values.paymentTerms ?? ''} maxlength={120} readonly={readOnly} />
|
|
175
|
+
</FormGroup>
|
|
176
|
+
<FormGroup label={t(M['commerce.vendor.default_contact_email'])} error={errors.fields?.defaultContactEmail}>
|
|
177
|
+
<Input name={names.defaultContactEmail} type="email" value={values.defaultContactEmail ?? ''} maxlength={320} readonly={readOnly} />
|
|
178
|
+
</FormGroup>
|
|
179
|
+
<FormGroup label={t(M['commerce.vendor.default_contact_phone'])} error={errors.fields?.defaultContactPhone}>
|
|
180
|
+
<Input name={names.defaultContactPhone} type="tel" value={values.defaultContactPhone ?? ''} maxlength={80} readonly={readOnly} />
|
|
181
|
+
</FormGroup>
|
|
182
|
+
</div>
|
|
183
|
+
<FormGroup label={t(M['commerce.party.internal_notes'])} error={errors.fields?.notes}>
|
|
184
|
+
<Textarea name={names.notes} value={values.notes ?? ''} rows={4} maxlength={4000} readonly={readOnly} />
|
|
185
|
+
</FormGroup>
|
|
186
|
+
</fieldset>
|
|
187
|
+
|
|
188
|
+
<PartyContactFields
|
|
189
|
+
contacts={values.contacts ?? []}
|
|
190
|
+
fieldNames={names}
|
|
191
|
+
errors={errors.contacts}
|
|
192
|
+
labels={contactLabels}
|
|
193
|
+
readOnly={readOnly}
|
|
194
|
+
canAdd={canAddContact}
|
|
195
|
+
canRemove={canRemoveContact}
|
|
196
|
+
{intentName}
|
|
197
|
+
addIntent={transport.addContactIntent}
|
|
198
|
+
removeIntent={transport.removeContactIntent}
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
{@render extension?.(values)}
|
|
202
|
+
|
|
203
|
+
{#if actions}
|
|
204
|
+
{@render actions()}
|
|
205
|
+
{:else if !readOnly && canSubmit}
|
|
206
|
+
<FormActionBar label={t(M['commerce.party.form_actions'], { singular })}>
|
|
207
|
+
{#if cancelHref}<Button href={cancelHref} variant="ghost">{labels.cancel ?? t(M['commerce.party.cancel'])}</Button>{/if}
|
|
208
|
+
<Button type="submit" name={intentName} value={transport.saveIntent ?? 'save'}>
|
|
209
|
+
{labels.save ?? (labels.singular ? t(M[mode === 'create' ? 'commerce.party.add_named' : 'commerce.party.save_named'], { singular: labels.singular }) : t(M[mode === 'create' ? 'commerce.vendor.add' : 'commerce.vendor.save']))}
|
|
210
|
+
</Button>
|
|
211
|
+
</FormActionBar>
|
|
212
|
+
{/if}
|
|
213
|
+
</Form>
|
|
214
|
+
|
|
215
|
+
<style>
|
|
216
|
+
:global(.party-form) { display: grid; gap: var(--smrt-spacing-5); min-inline-size: 0; padding-block-end: var(--smrt-spacing-8); }
|
|
217
|
+
.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; }
|
|
218
|
+
.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); }
|
|
219
|
+
.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); }
|
|
220
|
+
legend { padding-inline: var(--smrt-spacing-2); font: var(--smrt-typography-title-medium-font); color: var(--smrt-color-on-surface); }
|
|
221
|
+
.locked-value { color: var(--smrt-color-on-surface-variant); }
|
|
222
|
+
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--smrt-spacing-4); }
|
|
223
|
+
@media (max-width: 42rem) { .field-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
224
|
+
</style>
|