@adia-ai/web-modules 0.8.36 → 0.8.38
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/CHANGELOG.md +25 -0
- package/README.md +4 -4
- package/agent-admin/admin-roster/admin-roster.js +1 -1
- package/agent-admin/admin-settings/admin-settings.js +1 -1
- package/agent-admin/agent-admin/agent-admin.js +1 -1
- package/billing/billing-overview/billing-overview.a2ui.json +281 -0
- package/billing/billing-overview/billing-overview.class.js +838 -0
- package/billing/billing-overview/billing-overview.css +223 -0
- package/billing/billing-overview/billing-overview.js +16 -0
- package/billing/billing-overview/billing-overview.yaml +441 -0
- package/billing/index.js +21 -0
- package/billing/invoice-detail/invoice-detail.a2ui.json +276 -0
- package/billing/invoice-detail/invoice-detail.class.js +711 -0
- package/billing/invoice-detail/invoice-detail.css +305 -0
- package/billing/invoice-detail/invoice-detail.js +16 -0
- package/billing/invoice-detail/invoice-detail.yaml +411 -0
- package/billing/invoice-history/invoice-history.a2ui.json +315 -0
- package/billing/invoice-history/invoice-history.class.js +672 -0
- package/billing/invoice-history/invoice-history.css +130 -0
- package/billing/invoice-history/invoice-history.js +16 -0
- package/billing/invoice-history/invoice-history.yaml +398 -0
- package/billing/payment-method-form/payment-method-form.a2ui.json +310 -0
- package/billing/payment-method-form/payment-method-form.class.js +886 -0
- package/billing/payment-method-form/payment-method-form.css +149 -0
- package/billing/payment-method-form/payment-method-form.js +16 -0
- package/billing/payment-method-form/payment-method-form.yaml +430 -0
- package/billing/payment-method-list/payment-method-list.a2ui.json +283 -0
- package/billing/payment-method-list/payment-method-list.class.js +705 -0
- package/billing/payment-method-list/payment-method-list.css +128 -0
- package/billing/payment-method-list/payment-method-list.js +16 -0
- package/billing/payment-method-list/payment-method-list.yaml +355 -0
- package/billing/plan-picker/plan-picker.a2ui.json +349 -0
- package/billing/plan-picker/plan-picker.class.js +635 -0
- package/billing/plan-picker/plan-picker.css +227 -0
- package/billing/plan-picker/plan-picker.js +16 -0
- package/billing/plan-picker/plan-picker.yaml +453 -0
- package/chat/README.md +2 -2
- package/chat/chat-composer/chat-composer.js +2 -2
- package/chat/chat-empty/chat-empty.js +2 -2
- package/chat/chat-shell/chat-shell.js +6 -6
- package/chat/chat-sidebar/chat-sidebar.js +2 -2
- package/chat/chat-surfaces/chat-surfaces.js +3 -3
- package/chat/chat-thread/chat-thread.js +2 -2
- package/dashboard/dashboard-layout/dashboard-layout.a2ui.json +254 -0
- package/dashboard/dashboard-layout/dashboard-layout.css +190 -0
- package/dashboard/dashboard-layout/dashboard-layout.js +289 -0
- package/dashboard/dashboard-layout/dashboard-layout.yaml +374 -0
- package/dashboard/date-range-selector/date-range-selector.a2ui.json +236 -0
- package/dashboard/date-range-selector/date-range-selector.css +84 -0
- package/dashboard/date-range-selector/date-range-selector.js +686 -0
- package/dashboard/date-range-selector/date-range-selector.yaml +358 -0
- package/dashboard/index.js +14 -0
- package/dist/chat/chat-shell.min.js +1 -1
- package/dist/everything.min.js +121 -102
- package/editor/editor-canvas/editor-canvas.js +2 -2
- package/editor/editor-shell/editor-shell.js +2 -2
- package/editor/editor-sidebar/editor-sidebar.js +2 -2
- package/editor/editor-toolbar/editor-toolbar.js +2 -2
- package/feedback/confirm-dialog/confirm-dialog.a2ui.json +206 -0
- package/feedback/confirm-dialog/confirm-dialog.css +157 -0
- package/feedback/confirm-dialog/confirm-dialog.js +383 -0
- package/feedback/confirm-dialog/confirm-dialog.yaml +297 -0
- package/feedback/index.js +10 -0
- package/form/form-popover/form-popover.js +1 -1
- package/generative/index.d.ts +1 -1
- package/generative/index.js +2 -2
- package/onboarding/index.js +1 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.a2ui.json +263 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.css +157 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.js +428 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.yaml +236 -0
- package/package.json +99 -6
- package/runtime/a2ui-root/a2ui-root.js +3 -3
- package/runtime/gen-root/gen-root.js +5 -5
- package/settings/index.js +12 -0
- package/settings/integrations-page/integrations-page.a2ui.json +274 -0
- package/settings/integrations-page/integrations-page.css +177 -0
- package/settings/integrations-page/integrations-page.d.ts +77 -0
- package/settings/integrations-page/integrations-page.js +413 -0
- package/settings/integrations-page/integrations-page.yaml +317 -0
- package/settings/notification-preferences/notification-preferences.a2ui.json +245 -0
- package/settings/notification-preferences/notification-preferences.css +204 -0
- package/settings/notification-preferences/notification-preferences.js +622 -0
- package/settings/notification-preferences/notification-preferences.yaml +358 -0
- package/shell/admin-command/admin-command.js +2 -2
- package/shell/admin-shell/admin-shell.js +2 -2
- package/shell/admin-sidebar/admin-sidebar.js +2 -2
- package/shell/embed-shell/embed-shell.js +3 -3
- package/simple/simple-shell/simple-shell.js +1 -1
- package/theme/theme-panel/theme-panel.js +2 -2
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <invoice-history-ui> — Canonical billing-list composite.
|
|
3
|
+
*
|
|
4
|
+
* Pre-configures <table-ui> with invoice-shaped columns + status-badge
|
|
5
|
+
* cell renderer + currency formatter + row-click navigation, alongside
|
|
6
|
+
* an optional <table-toolbar-ui> for search/filter and a default
|
|
7
|
+
* "Export CSV" button. Wraps, does not fork, the underlying primitive.
|
|
8
|
+
*
|
|
9
|
+
* SPEC: .claude/docs/specs/implementation-ready/SPEC-008-invoice-history-table.md
|
|
10
|
+
* WAI-APG: https://www.w3.org/WAI/ARIA/apg/patterns/grid/ (inherited
|
|
11
|
+
* from <table-ui>)
|
|
12
|
+
*
|
|
13
|
+
* Architecture (light-DOM, ADR-0033):
|
|
14
|
+
* This composite stamps a three-region skeleton once on first connect:
|
|
15
|
+
* [data-toolbar] (slot toolbar / default toolbar)
|
|
16
|
+
* [data-table] (the <table-ui>)
|
|
17
|
+
* [data-empty] (default <empty-state-ui> branch)
|
|
18
|
+
* Then mutates the table's `.data` / `.columns` on every render. The
|
|
19
|
+
* table primitive owns row stamping; this composite only wires its
|
|
20
|
+
* inputs and converts cell renderers.
|
|
21
|
+
*
|
|
22
|
+
* Public API:
|
|
23
|
+
* .invoices — array of records (re-render on assign)
|
|
24
|
+
* .currency — default ISO 4217 code
|
|
25
|
+
* .filter — active status filter ('' = all)
|
|
26
|
+
* .setFilter(s) — set the filter programmatically; returns true on hit
|
|
27
|
+
* .exportCsv() — Blob of currently-visible rows
|
|
28
|
+
* .getSelected() — array of selected invoice records
|
|
29
|
+
* .refresh() — re-fetch from data-stream-src when configured
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { UIElement } from '@adia-ai/web-components/core/element';
|
|
33
|
+
import { untracked } from '@adia-ai/web-components/core/signals';
|
|
34
|
+
|
|
35
|
+
const KNOWN_STATUSES = new Set(['draft', 'open', 'paid', 'past-due', 'void']);
|
|
36
|
+
|
|
37
|
+
const STATUS_TO_VARIANT = {
|
|
38
|
+
paid: 'success',
|
|
39
|
+
open: 'info',
|
|
40
|
+
'past-due':'danger',
|
|
41
|
+
};
|
|
42
|
+
// `void` and `draft` are family-less badges (quiet chrome) — `variant="muted"`/
|
|
43
|
+
// `"neutral"` retired (variant/color/tone convention cut, gh#873/874); the
|
|
44
|
+
// status render() below sets `tone="muted"` for any status absent here.
|
|
45
|
+
|
|
46
|
+
const STATUS_LABEL = {
|
|
47
|
+
paid: 'Paid',
|
|
48
|
+
open: 'Open',
|
|
49
|
+
'past-due':'Past due',
|
|
50
|
+
void: 'Void',
|
|
51
|
+
draft: 'Draft',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const FILTER_CHIPS = [
|
|
55
|
+
{ value: '', label: 'All' },
|
|
56
|
+
{ value: 'paid', label: 'Paid' },
|
|
57
|
+
{ value: 'open', label: 'Open' },
|
|
58
|
+
{ value: 'past-due', label: 'Past due' },
|
|
59
|
+
{ value: 'void', label: 'Void' },
|
|
60
|
+
{ value: 'draft', label: 'Draft' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const CSV_HEADERS = ['Number', 'Issued', 'Due', 'Amount', 'Currency', 'Status'];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format an amount via Intl.NumberFormat. Falls back to `${currency}${value}`
|
|
67
|
+
* on engine failure.
|
|
68
|
+
*/
|
|
69
|
+
function formatAmount(amount, currency, locale) {
|
|
70
|
+
const value = Number.isFinite(amount) ? amount : 0;
|
|
71
|
+
try {
|
|
72
|
+
return new Intl.NumberFormat(locale || undefined, {
|
|
73
|
+
style: 'currency',
|
|
74
|
+
currency: currency || 'USD',
|
|
75
|
+
maximumFractionDigits: Number.isInteger(value) ? 0 : 2,
|
|
76
|
+
}).format(value);
|
|
77
|
+
} catch {
|
|
78
|
+
return `${currency || '$'}${value}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Format a date string ("YYYY-MM-DD" or ISO) via Intl.DateTimeFormat
|
|
84
|
+
* with the medium style. Returns the raw string on failure.
|
|
85
|
+
*/
|
|
86
|
+
function formatDate(raw, locale) {
|
|
87
|
+
if (!raw) return '';
|
|
88
|
+
try {
|
|
89
|
+
const d = raw instanceof Date ? raw : new Date(raw);
|
|
90
|
+
if (Number.isNaN(d.getTime())) return String(raw);
|
|
91
|
+
return new Intl.DateTimeFormat(locale || undefined, {
|
|
92
|
+
year: 'numeric',
|
|
93
|
+
month: 'short',
|
|
94
|
+
day: '2-digit',
|
|
95
|
+
}).format(d);
|
|
96
|
+
} catch {
|
|
97
|
+
return String(raw);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve hrefPattern with invoice data — replaces `{number}` and
|
|
103
|
+
* `{id}` tokens. Other tokens fall through unchanged.
|
|
104
|
+
*/
|
|
105
|
+
function resolveHref(pattern, invoice) {
|
|
106
|
+
const tpl = pattern || '/invoices/{number}';
|
|
107
|
+
return tpl
|
|
108
|
+
.replace('{number}', encodeURIComponent(invoice?.number ?? ''))
|
|
109
|
+
.replace('{id}', encodeURIComponent(invoice?.id ?? ''));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Escape a value for CSV output (Excel-friendly).
|
|
114
|
+
*/
|
|
115
|
+
function csvEscape(val) {
|
|
116
|
+
const str = String(val ?? '');
|
|
117
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
118
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
119
|
+
}
|
|
120
|
+
return str;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class InvoiceHistory extends UIElement {
|
|
124
|
+
static properties = {
|
|
125
|
+
invoices: { type: Array, default: [] },
|
|
126
|
+
currency: { type: String, default: 'USD', reflect: true },
|
|
127
|
+
density: { type: String, default: 'standard', reflect: true },
|
|
128
|
+
loading: { type: Boolean, default: false, reflect: true },
|
|
129
|
+
selectable: { type: Boolean, default: false, reflect: true },
|
|
130
|
+
hrefPattern: { type: String, default: '/invoices/{number}', reflect: true, attribute: 'href-pattern' },
|
|
131
|
+
paginate: { type: Number, default: 25, reflect: true },
|
|
132
|
+
exportable: { type: Boolean, default: true, reflect: true },
|
|
133
|
+
locale: { type: String, default: '' },
|
|
134
|
+
filter: { type: String, default: '', reflect: true },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// The composite stamps its own DOM (toolbar · table · empty) once on
|
|
138
|
+
// connect and mutates table inputs across renders — no html template.
|
|
139
|
+
static template = () => null;
|
|
140
|
+
|
|
141
|
+
static requiredIcons = ['download-simple', 'receipt', 'magnifying-glass'];
|
|
142
|
+
|
|
143
|
+
// ── Field refs ────────────────────────────────────────────────────
|
|
144
|
+
#stamped = false;
|
|
145
|
+
#cleanups = [];
|
|
146
|
+
#toolbarHost = null; // <div data-toolbar>
|
|
147
|
+
#defaultToolbar = null; // <table-toolbar-ui> (when no slot=toolbar provided)
|
|
148
|
+
#filterChipsHost = null; // <div data-invoice-filter>
|
|
149
|
+
#exportBtn = null; // <button-ui data-export>
|
|
150
|
+
#tableHost = null; // <div data-table-host>
|
|
151
|
+
#table = null; // <table-ui>
|
|
152
|
+
#emptyHost = null; // <empty-state-ui data-empty>
|
|
153
|
+
#internalUpdate = false;
|
|
154
|
+
#loadCount = 0; // monotonic guard against stale fetches
|
|
155
|
+
#warnedColumns = false;
|
|
156
|
+
|
|
157
|
+
// ── Public API ────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/** Set the active status filter. Returns true on hit. */
|
|
160
|
+
setFilter(value) {
|
|
161
|
+
const next = value || '';
|
|
162
|
+
if (next && !KNOWN_STATUSES.has(next)) return false;
|
|
163
|
+
return untracked(() => {
|
|
164
|
+
if (this.filter === next) return true;
|
|
165
|
+
const previous = this.filter;
|
|
166
|
+
this.filter = next;
|
|
167
|
+
this.dispatchEvent(new CustomEvent('invoice-filter-change', {
|
|
168
|
+
bubbles: true,
|
|
169
|
+
detail: { filter: next, previous },
|
|
170
|
+
}));
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** CSV blob of the currently-visible (filtered) rows. */
|
|
176
|
+
exportCsv() {
|
|
177
|
+
const rows = this.#visibleInvoices();
|
|
178
|
+
const lines = [CSV_HEADERS.join(',')];
|
|
179
|
+
for (const inv of rows) {
|
|
180
|
+
const cells = [
|
|
181
|
+
csvEscape(inv.number ?? ''),
|
|
182
|
+
csvEscape(inv.issuedAt ?? ''),
|
|
183
|
+
csvEscape(inv.dueAt ?? ''),
|
|
184
|
+
csvEscape(Number.isFinite(inv.amount) ? inv.amount : ''),
|
|
185
|
+
csvEscape(inv.currency || this.currency || 'USD'),
|
|
186
|
+
csvEscape(inv.status ?? ''),
|
|
187
|
+
];
|
|
188
|
+
lines.push(cells.join(','));
|
|
189
|
+
}
|
|
190
|
+
const csv = lines.join('\r\n');
|
|
191
|
+
return new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Array of selected invoice records (when `selectable` is set). */
|
|
195
|
+
getSelected() {
|
|
196
|
+
if (!this.#table) return [];
|
|
197
|
+
const indices = this.#table.selected || [];
|
|
198
|
+
const rows = this.#visibleInvoices();
|
|
199
|
+
return indices.map((i) => rows[i]).filter(Boolean);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Re-fetch invoices from `data-stream-src`. */
|
|
203
|
+
async refresh() {
|
|
204
|
+
const src = this.getAttribute('data-stream-src');
|
|
205
|
+
if (!src) return;
|
|
206
|
+
const tag = ++this.#loadCount;
|
|
207
|
+
this.setAttribute('loading', '');
|
|
208
|
+
this.removeAttribute('error');
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(src);
|
|
211
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
212
|
+
const data = await res.json();
|
|
213
|
+
if (tag !== this.#loadCount) return; // stale
|
|
214
|
+
this.removeAttribute('loading');
|
|
215
|
+
const list = Array.isArray(data)
|
|
216
|
+
? data
|
|
217
|
+
: (Array.isArray(data?.invoices) ? data.invoices : []);
|
|
218
|
+
this.invoices = list;
|
|
219
|
+
this.dispatchEvent(new CustomEvent('load', {
|
|
220
|
+
bubbles: true,
|
|
221
|
+
detail: { count: list.length },
|
|
222
|
+
}));
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (tag !== this.#loadCount) return;
|
|
225
|
+
this.removeAttribute('loading');
|
|
226
|
+
this.setAttribute('error', '');
|
|
227
|
+
this.dispatchEvent(new CustomEvent('error', {
|
|
228
|
+
bubbles: true,
|
|
229
|
+
detail: { reason: 'fetch-failed', message: String(err?.message || err) },
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Lifecycle ─────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
connected() {
|
|
237
|
+
super.connected?.();
|
|
238
|
+
this.setAttribute('role', 'region');
|
|
239
|
+
if (!this.hasAttribute('aria-label') && !this.hasAttribute('aria-labelledby')) {
|
|
240
|
+
this.setAttribute('aria-label', 'Invoice history');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!this.#stamped) {
|
|
244
|
+
this.#absorbInlineInvoicesScript();
|
|
245
|
+
this.#stamp();
|
|
246
|
+
this.#wireListeners();
|
|
247
|
+
this.#stamped = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
untracked(() => this.render());
|
|
251
|
+
|
|
252
|
+
if (this.getAttribute('data-stream-src')) {
|
|
253
|
+
this.refresh();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
disconnected() {
|
|
258
|
+
super.disconnected?.();
|
|
259
|
+
for (const fn of this.#cleanups) {
|
|
260
|
+
try { fn(); } catch { /* ignore stale */ }
|
|
261
|
+
}
|
|
262
|
+
this.#cleanups.length = 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
266
|
+
super.attributeChangedCallback?.(name, oldVal, newVal);
|
|
267
|
+
if (!this.#stamped || this.#internalUpdate) return;
|
|
268
|
+
if (name === 'data-stream-src' && oldVal !== newVal && newVal) {
|
|
269
|
+
this.refresh();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Render — narrow; mutates per-region state ─────────────────────
|
|
274
|
+
|
|
275
|
+
render() {
|
|
276
|
+
if (!this.#stamped) return;
|
|
277
|
+
|
|
278
|
+
const list = Array.isArray(this.invoices) ? this.invoices : [];
|
|
279
|
+
|
|
280
|
+
// Empty / non-empty state attribute (CSS-gated reveal).
|
|
281
|
+
if (list.length === 0 && !this.loading) {
|
|
282
|
+
this.setAttribute('empty', '');
|
|
283
|
+
} else {
|
|
284
|
+
this.removeAttribute('empty');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Refresh filter chip selection.
|
|
288
|
+
this.#renderFilterChips();
|
|
289
|
+
|
|
290
|
+
// Push columns + filtered data to the underlying table.
|
|
291
|
+
if (this.#table) {
|
|
292
|
+
this.#table.columns = this.#deriveColumns();
|
|
293
|
+
this.#table.data = this.#visibleInvoices();
|
|
294
|
+
// Forward forwarding-only props. Reading a signal-backed property on
|
|
295
|
+
// the CHILD (table-ui's own `density`/`loading`/`paginate`/`selectable`)
|
|
296
|
+
// while THIS element's own render effect is tracking would silently
|
|
297
|
+
// subscribe invoice-history's effect to table-ui's internal signals —
|
|
298
|
+
// this composite is a one-way source of truth for these forwarded
|
|
299
|
+
// props and must never react to the table's copies (gh#661). untracked()
|
|
300
|
+
// keeps the compare-before-write idempotent without leaking that
|
|
301
|
+
// cross-element subscription; invoice-detail.class.js avoids the same
|
|
302
|
+
// hazard by comparing via getAttribute() instead.
|
|
303
|
+
untracked(() => {
|
|
304
|
+
if (this.#table.density !== this.density) this.#table.density = this.density;
|
|
305
|
+
if (!!this.#table.loading !== !!this.loading) this.#table.loading = !!this.loading;
|
|
306
|
+
if (Number(this.#table.paginate || 0) !== Number(this.paginate || 0)) {
|
|
307
|
+
this.#table.paginate = Number(this.paginate || 0);
|
|
308
|
+
}
|
|
309
|
+
if (!!this.#table.selectable !== !!this.selectable) this.#table.selectable = !!this.selectable;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Toolbar count: forward visible row count to default toolbar.
|
|
314
|
+
if (this.#defaultToolbar) {
|
|
315
|
+
this.#defaultToolbar.setAttribute('count', String(this.#visibleInvoices().length));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Export button visibility tracks `exportable`.
|
|
319
|
+
if (this.#exportBtn) {
|
|
320
|
+
this.#exportBtn.hidden = !this.exportable;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Internal — DOM stamping ───────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
#absorbInlineInvoicesScript() {
|
|
327
|
+
const script = this.querySelector(':scope > script[type="application/json"][data-invoices]');
|
|
328
|
+
if (!script) return;
|
|
329
|
+
try {
|
|
330
|
+
const parsed = JSON.parse(script.textContent || '[]');
|
|
331
|
+
if (Array.isArray(parsed)) this.invoices = parsed;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
// eslint-disable-next-line no-console
|
|
334
|
+
console.warn('[invoice-history-ui] failed to parse <script data-invoices> JSON:', err?.message);
|
|
335
|
+
} finally {
|
|
336
|
+
script.remove();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#stamp() {
|
|
341
|
+
// ── Toolbar region ─────────────────────────────────────────────
|
|
342
|
+
const toolbar = document.createElement('div');
|
|
343
|
+
toolbar.setAttribute('data-toolbar', '');
|
|
344
|
+
this.appendChild(toolbar);
|
|
345
|
+
this.#toolbarHost = toolbar;
|
|
346
|
+
|
|
347
|
+
const userToolbar = this.querySelector(':scope > [slot="toolbar"]');
|
|
348
|
+
if (userToolbar) {
|
|
349
|
+
toolbar.appendChild(userToolbar);
|
|
350
|
+
} else {
|
|
351
|
+
// Default: <table-toolbar-ui> with title + count + filter chips +
|
|
352
|
+
// optional export button. The toolbar is wired by [for] when the
|
|
353
|
+
// table is stamped below.
|
|
354
|
+
const tt = document.createElement('table-toolbar-ui');
|
|
355
|
+
tt.setAttribute('data-default-toolbar', '');
|
|
356
|
+
tt.setAttribute('text', 'Invoices');
|
|
357
|
+
// Status-filter chips live in the [slot="actions"] region so they
|
|
358
|
+
// sit alongside the toolbar's title + search affordances.
|
|
359
|
+
const chips = document.createElement('div');
|
|
360
|
+
chips.setAttribute('data-invoice-filter', '');
|
|
361
|
+
chips.setAttribute('role', 'group');
|
|
362
|
+
chips.setAttribute('aria-label', 'Filter invoices by status');
|
|
363
|
+
chips.setAttribute('slot', 'actions');
|
|
364
|
+
tt.appendChild(chips);
|
|
365
|
+
|
|
366
|
+
const btn = document.createElement('button-ui');
|
|
367
|
+
btn.setAttribute('data-export', '');
|
|
368
|
+
btn.setAttribute('slot', 'actions');
|
|
369
|
+
btn.setAttribute('variant', 'outline');
|
|
370
|
+
btn.setAttribute('size', 'sm');
|
|
371
|
+
btn.setAttribute('text', 'Export CSV');
|
|
372
|
+
btn.setAttribute('icon', 'download-simple');
|
|
373
|
+
tt.appendChild(btn);
|
|
374
|
+
|
|
375
|
+
toolbar.appendChild(tt);
|
|
376
|
+
this.#defaultToolbar = tt;
|
|
377
|
+
this.#filterChipsHost = chips;
|
|
378
|
+
this.#exportBtn = btn;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Table region ───────────────────────────────────────────────
|
|
382
|
+
const tableHost = document.createElement('div');
|
|
383
|
+
tableHost.setAttribute('data-table-host', '');
|
|
384
|
+
this.appendChild(tableHost);
|
|
385
|
+
this.#tableHost = tableHost;
|
|
386
|
+
|
|
387
|
+
const table = document.createElement('table-ui');
|
|
388
|
+
table.id = this.id ? `${this.id}__table` : `invoice-history-${Math.random().toString(36).slice(2, 8)}`;
|
|
389
|
+
table.setAttribute('data-invoice-table', '');
|
|
390
|
+
table.setAttribute('sortable', '');
|
|
391
|
+
tableHost.appendChild(table);
|
|
392
|
+
this.#table = table;
|
|
393
|
+
|
|
394
|
+
// Wire the default toolbar to this table by id.
|
|
395
|
+
if (this.#defaultToolbar) {
|
|
396
|
+
this.#defaultToolbar.setAttribute('for', table.id);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Empty-state region ─────────────────────────────────────────
|
|
400
|
+
if (!this.querySelector(':scope > [slot="empty"]')) {
|
|
401
|
+
const empty = document.createElement('empty-state-ui');
|
|
402
|
+
empty.setAttribute('data-empty', '');
|
|
403
|
+
empty.setAttribute('icon', 'receipt');
|
|
404
|
+
empty.setAttribute('heading', 'No invoices yet');
|
|
405
|
+
empty.setAttribute('description', 'Invoices will appear here once issued.');
|
|
406
|
+
this.appendChild(empty);
|
|
407
|
+
this.#emptyHost = empty;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#renderFilterChips() {
|
|
412
|
+
if (!this.#filterChipsHost) return;
|
|
413
|
+
if (this.#filterChipsHost.children.length === 0) {
|
|
414
|
+
for (const chip of FILTER_CHIPS) {
|
|
415
|
+
const btn = document.createElement('button-ui');
|
|
416
|
+
btn.setAttribute('data-filter-chip', '');
|
|
417
|
+
btn.setAttribute('data-filter-value', chip.value);
|
|
418
|
+
btn.setAttribute('size', 'sm');
|
|
419
|
+
btn.setAttribute('text', chip.label);
|
|
420
|
+
this.#filterChipsHost.appendChild(btn);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Sync `variant` to active state.
|
|
424
|
+
const active = this.filter || '';
|
|
425
|
+
for (const btn of this.#filterChipsHost.querySelectorAll('[data-filter-chip]')) {
|
|
426
|
+
const value = btn.getAttribute('data-filter-value') || '';
|
|
427
|
+
if (value === active) {
|
|
428
|
+
btn.setAttribute('variant', 'primary');
|
|
429
|
+
btn.setAttribute('aria-pressed', 'true');
|
|
430
|
+
} else {
|
|
431
|
+
btn.setAttribute('variant', 'ghost');
|
|
432
|
+
btn.setAttribute('aria-pressed', 'false');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Derivation helpers ─────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
#visibleInvoices() {
|
|
440
|
+
const list = Array.isArray(this.invoices) ? this.invoices : [];
|
|
441
|
+
const active = this.filter || '';
|
|
442
|
+
if (!active) return list;
|
|
443
|
+
return list.filter((inv) => (inv?.status || '') === active);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
#deriveColumns() {
|
|
447
|
+
const locale = this.locale || (typeof document !== 'undefined' ? (document.documentElement?.lang || '') : '');
|
|
448
|
+
const defaultCurrency = this.currency || 'USD';
|
|
449
|
+
|
|
450
|
+
return [
|
|
451
|
+
{
|
|
452
|
+
key: 'number',
|
|
453
|
+
label: 'Invoice',
|
|
454
|
+
sortable: true,
|
|
455
|
+
wrap: false,
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
key: 'issuedAt',
|
|
459
|
+
label: 'Issued',
|
|
460
|
+
sortable: true,
|
|
461
|
+
format: (value) => formatDate(value, locale),
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
key: 'dueAt',
|
|
465
|
+
label: 'Due',
|
|
466
|
+
sortable: true,
|
|
467
|
+
format: (value) => formatDate(value, locale),
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
key: 'amount',
|
|
471
|
+
label: 'Amount',
|
|
472
|
+
sortable: true,
|
|
473
|
+
align: 'end',
|
|
474
|
+
render: (value, row, cell) => {
|
|
475
|
+
const currency = row?.currency || defaultCurrency;
|
|
476
|
+
const span = document.createElement('span');
|
|
477
|
+
span.setAttribute('data-invoice-amount', '');
|
|
478
|
+
span.textContent = formatAmount(value, currency, locale);
|
|
479
|
+
// Cell base alignment cue (also driven by CSS).
|
|
480
|
+
cell.dataset.align = 'end';
|
|
481
|
+
return span;
|
|
482
|
+
},
|
|
483
|
+
format: (value, row) => formatAmount(value, row?.currency || defaultCurrency, locale),
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
key: 'status',
|
|
487
|
+
label: 'Status',
|
|
488
|
+
sortable: true,
|
|
489
|
+
render: (value) => {
|
|
490
|
+
const key = String(value || '').toLowerCase();
|
|
491
|
+
const variant = STATUS_TO_VARIANT[key];
|
|
492
|
+
const label = STATUS_LABEL[key] || (value ? String(value) : '—');
|
|
493
|
+
const badge = document.createElement('badge-ui');
|
|
494
|
+
badge.setAttribute('data-invoice-status', '');
|
|
495
|
+
if (variant) {
|
|
496
|
+
badge.setAttribute('variant', variant);
|
|
497
|
+
} else {
|
|
498
|
+
badge.setAttribute('tone', 'muted');
|
|
499
|
+
}
|
|
500
|
+
badge.setAttribute('text', label);
|
|
501
|
+
badge.setAttribute('aria-label', label);
|
|
502
|
+
return badge;
|
|
503
|
+
},
|
|
504
|
+
format: (value) => STATUS_LABEL[String(value || '').toLowerCase()] || (value ? String(value) : ''),
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
key: 'actions',
|
|
508
|
+
label: '',
|
|
509
|
+
sortable: false,
|
|
510
|
+
align: 'end',
|
|
511
|
+
accessor: (row) => row,
|
|
512
|
+
render: (_value, row) => {
|
|
513
|
+
const wrap = document.createElement('span');
|
|
514
|
+
wrap.setAttribute('data-invoice-actions', '');
|
|
515
|
+
|
|
516
|
+
const viewBtn = document.createElement('button-ui');
|
|
517
|
+
viewBtn.setAttribute('data-invoice-action', 'view');
|
|
518
|
+
viewBtn.setAttribute('data-invoice-id', String(row?.id ?? row?.number ?? ''));
|
|
519
|
+
viewBtn.setAttribute('size', 'sm');
|
|
520
|
+
viewBtn.setAttribute('variant', 'ghost');
|
|
521
|
+
viewBtn.setAttribute('text', 'View');
|
|
522
|
+
wrap.appendChild(viewBtn);
|
|
523
|
+
|
|
524
|
+
const dlBtn = document.createElement('button-ui');
|
|
525
|
+
dlBtn.setAttribute('data-invoice-action', 'download');
|
|
526
|
+
dlBtn.setAttribute('data-invoice-id', String(row?.id ?? row?.number ?? ''));
|
|
527
|
+
dlBtn.setAttribute('size', 'sm');
|
|
528
|
+
dlBtn.setAttribute('variant', 'ghost');
|
|
529
|
+
dlBtn.setAttribute('icon', 'download-simple');
|
|
530
|
+
dlBtn.setAttribute('aria-label', `Download invoice ${row?.number ?? ''}`);
|
|
531
|
+
wrap.appendChild(dlBtn);
|
|
532
|
+
|
|
533
|
+
return wrap;
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ── Listener wiring ────────────────────────────────────────────────
|
|
540
|
+
|
|
541
|
+
#wireListeners() {
|
|
542
|
+
// ── Filter chip clicks ──
|
|
543
|
+
const onChipClick = (e) => {
|
|
544
|
+
const chip = e.target?.closest?.('[data-filter-chip]');
|
|
545
|
+
if (!chip || !this.#filterChipsHost?.contains(chip)) return;
|
|
546
|
+
e.stopPropagation();
|
|
547
|
+
const value = chip.getAttribute('data-filter-value') || '';
|
|
548
|
+
this.setFilter(value);
|
|
549
|
+
};
|
|
550
|
+
this.addEventListener('press', onChipClick);
|
|
551
|
+
this.addEventListener('click', onChipClick);
|
|
552
|
+
this.#cleanups.push(() => this.removeEventListener('press', onChipClick));
|
|
553
|
+
this.#cleanups.push(() => this.removeEventListener('click', onChipClick));
|
|
554
|
+
|
|
555
|
+
// ── Row activation (cell-click excluding actions column) ──
|
|
556
|
+
const onCellClick = (e) => {
|
|
557
|
+
const detail = e?.detail;
|
|
558
|
+
if (!detail) return;
|
|
559
|
+
// Actions column has its own buttons — don't navigate when clicking them.
|
|
560
|
+
if (detail.key === 'actions') return;
|
|
561
|
+
const row = detail.row;
|
|
562
|
+
if (!row) return;
|
|
563
|
+
const href = resolveHref(this.hrefPattern, row);
|
|
564
|
+
const ev = new CustomEvent('invoice-row-click', {
|
|
565
|
+
bubbles: true,
|
|
566
|
+
cancelable: true,
|
|
567
|
+
detail: { invoice: row, href },
|
|
568
|
+
});
|
|
569
|
+
const proceed = this.dispatchEvent(ev);
|
|
570
|
+
if (proceed && typeof window !== 'undefined' && window.location?.assign) {
|
|
571
|
+
try { window.location.assign(href); } catch { /* test envs */ }
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
this.addEventListener('cell-click', onCellClick);
|
|
575
|
+
this.#cleanups.push(() => this.removeEventListener('cell-click', onCellClick));
|
|
576
|
+
|
|
577
|
+
// ── Per-row action buttons ──
|
|
578
|
+
const onActionClick = (e) => {
|
|
579
|
+
const btn = e.target?.closest?.('button-ui[data-invoice-action]');
|
|
580
|
+
if (!btn) return;
|
|
581
|
+
// Ensure we're inside our table region (not the toolbar or empty).
|
|
582
|
+
if (!this.#tableHost?.contains(btn)) return;
|
|
583
|
+
e.stopPropagation();
|
|
584
|
+
const action = btn.getAttribute('data-invoice-action');
|
|
585
|
+
const invoiceId = btn.getAttribute('data-invoice-id') || '';
|
|
586
|
+
const row = (this.invoices || []).find(
|
|
587
|
+
(r) => String(r?.id ?? r?.number ?? '') === invoiceId,
|
|
588
|
+
);
|
|
589
|
+
if (!row) return;
|
|
590
|
+
if (action === 'view') {
|
|
591
|
+
const href = resolveHref(this.hrefPattern, row);
|
|
592
|
+
const ev = new CustomEvent('invoice-row-click', {
|
|
593
|
+
bubbles: true,
|
|
594
|
+
cancelable: true,
|
|
595
|
+
detail: { invoice: row, href },
|
|
596
|
+
});
|
|
597
|
+
const proceed = this.dispatchEvent(ev);
|
|
598
|
+
if (proceed && typeof window !== 'undefined' && window.location?.assign) {
|
|
599
|
+
try { window.location.assign(href); } catch { /* test envs */ }
|
|
600
|
+
}
|
|
601
|
+
} else if (action === 'download') {
|
|
602
|
+
const blob = this.#exportSingleInvoiceCsv(row);
|
|
603
|
+
this.dispatchEvent(new CustomEvent('invoice-export', {
|
|
604
|
+
bubbles: true,
|
|
605
|
+
cancelable: true,
|
|
606
|
+
detail: { blob, rowCount: 1, invoice: row },
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
this.addEventListener('press', onActionClick);
|
|
611
|
+
this.addEventListener('click', onActionClick);
|
|
612
|
+
this.#cleanups.push(() => this.removeEventListener('press', onActionClick));
|
|
613
|
+
this.#cleanups.push(() => this.removeEventListener('click', onActionClick));
|
|
614
|
+
|
|
615
|
+
// ── Export CSV button ──
|
|
616
|
+
const onExport = (e) => {
|
|
617
|
+
const btn = e.target?.closest?.('button-ui[data-export]');
|
|
618
|
+
if (!btn || !this.#toolbarHost?.contains(btn)) return;
|
|
619
|
+
e.stopPropagation();
|
|
620
|
+
const blob = this.exportCsv();
|
|
621
|
+
const ev = new CustomEvent('invoice-export', {
|
|
622
|
+
bubbles: true,
|
|
623
|
+
cancelable: true,
|
|
624
|
+
detail: { blob, rowCount: this.#visibleInvoices().length },
|
|
625
|
+
});
|
|
626
|
+
const proceed = this.dispatchEvent(ev);
|
|
627
|
+
if (proceed && typeof document !== 'undefined' && typeof URL !== 'undefined') {
|
|
628
|
+
try {
|
|
629
|
+
const url = URL.createObjectURL(blob);
|
|
630
|
+
const a = document.createElement('a');
|
|
631
|
+
a.href = url;
|
|
632
|
+
a.download = 'invoices.csv';
|
|
633
|
+
document.body.appendChild(a);
|
|
634
|
+
a.click();
|
|
635
|
+
a.remove();
|
|
636
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
637
|
+
} catch { /* download not available */ }
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
this.addEventListener('press', onExport);
|
|
641
|
+
this.addEventListener('click', onExport);
|
|
642
|
+
this.#cleanups.push(() => this.removeEventListener('press', onExport));
|
|
643
|
+
this.#cleanups.push(() => this.removeEventListener('click', onExport));
|
|
644
|
+
|
|
645
|
+
// ── Selection forwarding ──
|
|
646
|
+
const onSelect = (e) => {
|
|
647
|
+
if (e.target !== this.#table) return;
|
|
648
|
+
const selected = this.getSelected();
|
|
649
|
+
this.dispatchEvent(new CustomEvent('invoice-selection-change', {
|
|
650
|
+
bubbles: true,
|
|
651
|
+
detail: { selected },
|
|
652
|
+
}));
|
|
653
|
+
};
|
|
654
|
+
this.addEventListener('select', onSelect);
|
|
655
|
+
this.#cleanups.push(() => this.removeEventListener('select', onSelect));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
#exportSingleInvoiceCsv(inv) {
|
|
659
|
+
const lines = [
|
|
660
|
+
CSV_HEADERS.join(','),
|
|
661
|
+
[
|
|
662
|
+
csvEscape(inv.number ?? ''),
|
|
663
|
+
csvEscape(inv.issuedAt ?? ''),
|
|
664
|
+
csvEscape(inv.dueAt ?? ''),
|
|
665
|
+
csvEscape(Number.isFinite(inv.amount) ? inv.amount : ''),
|
|
666
|
+
csvEscape(inv.currency || this.currency || 'USD'),
|
|
667
|
+
csvEscape(inv.status ?? ''),
|
|
668
|
+
].join(','),
|
|
669
|
+
];
|
|
670
|
+
return new Blob([lines.join('\r\n')], { type: 'text/csv;charset=utf-8;' });
|
|
671
|
+
}
|
|
672
|
+
}
|