@subvalue/cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,17 @@
1
1
 
2
2
  import { product, productMark, productNameMarkup } from '../../../packages/ui/src/brand.js';
3
3
  document.title = product.name + ' — Local usage';
4
-
4
+
5
+
6
+
7
+
8
+
9
+
10
+ import {
11
+ billingForSelection,
12
+ withPeriodBilling,
13
+ showExpenseAction,
14
+ } from '../../../packages/core/src/billing.js';
5
15
  import {
6
16
  subscriptionPlans,
7
17
  subscriptionPlan,
@@ -60,6 +70,8 @@ const icon = (key ) => {
60
70
  <path d="${paths[key] ?? paths.token}" />
61
71
  </svg>`;
62
72
  };
73
+ let expenseProvider = null;
74
+ let expensesSaving = false;
63
75
  let settings ,
64
76
  sources = [],
65
77
  summary ,
@@ -521,6 +533,17 @@ function providerCards() {
521
533
  >
522
534
  </div>
523
535
  </div>
536
+ ${
537
+ showExpenseAction(p, summary.providers)
538
+ ? `<button type="button" class="provider-expense-toggle" data-expense-toggle="${p.provider}" aria-controls="card-expenses-${p.provider}" aria-expanded="false"><span>Categorize expenses for this period</span> ${icon('next')}</button>
539
+ <div id="card-expenses-${p.provider}" class="provider-expenses-popover" popover aria-label="${name(p.provider)} expenses">
540
+ <div class="expenses-heading"><h2>${name(p.provider)} · Expenses</h2><button type="button" class="text-button" popovertarget="card-expenses-${p.provider}" popovertargetaction="hide" aria-label="Close expenses">×</button></div>
541
+ <form class="provider-expenses-form" data-period-billing="${p.provider}">
542
+ ${billingFields(p.provider, true, billingForSelection(settings, p.provider, summary.range), true, 'card-')}
543
+ <div class="expense-actions"><small>${esc(periodLabel())}</small><button class="button small" type="submit" ${expensesSaving ? 'disabled' : ''}>Save</button></div>
544
+ <p class="billing-error" role="alert" hidden></p></form></div>`
545
+ : ''
546
+ }
524
547
  </article>`,
525
548
  )
526
549
  .join('')}
@@ -570,23 +593,55 @@ function overview() {
570
593
  c = comparisonForDisplay(summary);
571
594
  const active = summary.providers.filter((p) => p.records);
572
595
  const months = fullCalendarMonths(summary.range),
573
- showSubs = months !== null && active.length && active.every((p) => p.mode === 'SUBSCRIPTION');
596
+ showSubs = months !== null && active.length && (c.paid != null || c.scope.length > 0),
597
+ scopeLabel = c.scope.map(name).join(' + '),
598
+ first = summary.providers
599
+ .map((p) => p.first)
600
+ .filter((value) => !!value)
601
+ .sort()[0],
602
+ last = summary.providers
603
+ .map((p) => p.last)
604
+ .filter((value) => !!value)
605
+ .sort()
606
+ .at(-1),
607
+ historyDate = (value ) =>
608
+ new Date(value).toLocaleDateString('en-US', {
609
+ month: 'short',
610
+ day: 'numeric',
611
+ year: 'numeric',
612
+ timeZone: 'UTC',
613
+ });
574
614
  return /* HTML */ `<div class="page-heading">
575
- <div><h1>Overview</h1></div>
615
+ <div class="overview-title">
616
+ <h1>Overview</h1>
617
+ <span class="selected-period" aria-live="polite">${esc(periodLabel())}</span>
618
+ </div>
576
619
  ${refreshButton()}
577
620
  </div>
578
621
  ${coverage()}
579
622
  <div class="overview-layout">
580
623
  <div class="overview-main">
581
624
  <div class="metrics ${showSubs ? '' : 'single'}">
582
- ${metric('API equivalent', t.apiEquivalent === null ? (t.knownSubtotal !== null ? money(t.knownSubtotal) : t.records ? '—' : 'No data') : money(t.apiEquivalent), periodLabel(), 'api-metric')}${showSubs ? metric('Subscription', c.subscription === null ? 'Not set' : money(c.subscription), months === 1 ? 'Full month' : `${months} full months`) : ''}${showSubs ? metric(summary.historyPartial ? 'Observed value' : 'Value', c.value === null ? '—' : money(c.value, true), c.value === null ? 'Comparison unavailable' : summary.historyPartial ? 'Partial history' : '', c.value !== null ? (c.value >= 0 ? 'positive' : 'negative') : '') : ''}${showSubs ? metric('Value multiple', c.roi === null ? '—' : `${c.roi.toFixed(2)}<span class="multiply">×</span>`, c.breakEven ? `Break-even ${new Date(c.breakEven + 'T12:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` : c.roi === null ? 'Unavailable' : t.apiEquivalent === null ? 'Priced portion' : '') : ''}
625
+ ${metric('API equivalent', t.apiEquivalent === null ? (t.knownSubtotal !== null ? money(t.knownSubtotal) : t.records ? '—' : 'No data') : money(t.apiEquivalent), periodLabel(), 'api-metric')}${showSubs ? metric(c.apiSpend ? 'You paid' : c.subscription === 0 ? 'API paid' : 'Subscription', money(c.paid ?? c.subscription), [scopeLabel, months === 1 ? 'Full month' : `${months} full months`].filter(Boolean).join(' · ')) : ''}${showSubs ? metric(summary.historyPartial ? 'Observed value' : 'Value', c.value === null ? '—' : money(c.value, true), [scopeLabel, c.value === null ? 'Comparison unavailable' : summary.historyPartial ? 'Partial history' : ''].filter(Boolean).join(' · '), c.value !== null ? (c.value >= 0 ? 'positive' : 'negative') : '') : ''}${showSubs ? metric('Value multiple', c.roi === null ? '—' : `${c.roi.toFixed(2)}<span class="multiply">×</span>`, [scopeLabel, c.breakEven ? `Break-even ${new Date(c.breakEven + 'T12:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` : c.roi === null ? 'Unavailable' : t.apiEquivalent === null ? 'Priced portion' : ''].filter(Boolean).join(' · ')) : ''}
583
626
  </div>
584
627
  ${providerCards()}${chart()}
585
628
  <div class="overview-bottom">
586
629
  <span>${icon('shield')}Local analysis. Read-only sources.</span
587
630
  ><button class="text-button" data-page="settings">Manage sources ${icon('arrow')}</button>
588
631
  </div>
589
- ${!t.records ? '<section class="panel empty-panel"><h2>No data for this period</h2><button class="text-button" data-page="settings">View sources →</button></section>' : ''}
632
+ ${
633
+ !t.records
634
+ ? /* HTML */ `<section class="panel empty-panel">
635
+ <h2>No data for this period</h2>
636
+ ${first && last ? `<p class="empty-history">Available history · ${esc(historyDate(first))} – ${esc(historyDate(last))}</p>` : ''}<button
637
+ class="text-button"
638
+ data-page="settings"
639
+ >
640
+ View sources →
641
+ </button>
642
+ </section>`
643
+ : ''
644
+ }
590
645
  </div>
591
646
  ${receiptPanel(true)}
592
647
  </div>`;
@@ -612,40 +667,52 @@ function receiptPage() {
612
667
  </aside>
613
668
  </div>`;
614
669
  }
615
- function billingFields(p , firstRun = false) {
616
- const b = settings.billing[p],
617
- plan = subscriptionPlan(p, b.planId);
618
- return /* HTML */ `<div class="billing-row">
619
- <label class="provider-setting-name" for="mode-${p}">${name(p)}</label
620
- ><select
621
- id="mode-${p}"
622
- name="mode-${p}"
623
- data-billing-mode="${p}"
624
- ${firstRun ? 'required data-first-run="true"' : ''}
625
- >
626
- <option value="${firstRun ? '' : 'UNKNOWN'}" ${b.mode === 'UNKNOWN' ? 'selected' : ''}>
627
- ${firstRun ? 'Choose usage' : 'Not set'}
628
- </option>
629
- <option value="SUBSCRIPTION" ${b.mode === 'SUBSCRIPTION' ? 'selected' : ''}>
630
- Subscription
631
- </option>
632
- <option value="API" ${b.mode === 'API' ? 'selected' : ''}>API</option>
633
- <option value="MIXED" ${b.mode === 'MIXED' ? 'selected' : ''}>Mixed / not sure</option>
634
- </select>
635
- <div class="subscription-fields ${b.mode === 'SUBSCRIPTION' ? '' : 'hidden'}" id="amount-${p}">
636
- <label class="sr-only" for="plan-${p}">${name(p)} subscription plan</label
637
- ><select id="plan-${p}" name="plan-${p}" data-subscription-plan="${p}">
638
- <option value="custom" ${!plan ? 'selected' : ''}>Custom amount</option>
670
+ function billingFields(
671
+ p ,
672
+ firstRun = false,
673
+ b = settings.billing[p],
674
+ expenses = false,
675
+ prefix = '',
676
+ ) {
677
+ const plan = subscriptionPlan(p, b.planId),
678
+ subscribed = b.mode === 'SUBSCRIPTION' || b.mode === 'MIXED',
679
+ selection = subscribed
680
+ ? (plan?.id ?? 'custom')
681
+ : b.mode === 'NO_SUBSCRIPTION' || b.mode === 'API'
682
+ ? 'none'
683
+ : '';
684
+ const api = !subscribed || b.mode === 'MIXED';
685
+ return /* HTML */ `<div class="billing-row" data-billing-provider="${p}">
686
+ <label class="provider-setting-name" for="${prefix}plan-${p}">${name(p)}</label>
687
+ <div class="subscription-fields">
688
+ <label class="sr-only" for="${prefix}plan-${p}">${name(p)} subscription plan</label
689
+ ><select
690
+ id="${prefix}plan-${p}"
691
+ name="plan-${p}"
692
+ data-subscription-plan="${p}"
693
+ ${firstRun ? 'required data-first-run="true"' : ''}
694
+ >
695
+ <option value="" ${selection === '' ? 'selected' : ''}>
696
+ ${firstRun ? 'Choose subscription' : 'Not set'}
697
+ </option>
698
+ <option value="none" ${selection === 'none' ? 'selected' : ''}>
699
+ No subscription · API
700
+ </option>
639
701
  ${subscriptionPlans
640
702
  .filter((plan) => plan.provider === p)
641
703
  .map(
642
704
  (option) =>
643
- /* HTML */ `<option value="${option.id}" ${plan?.id === option.id ? 'selected' : ''}>
705
+ /* HTML */ `<option value="${option.id}" ${selection === option.id ? 'selected' : ''}>
644
706
  ${esc(subscriptionPlanLabel(option))}
645
707
  </option>`,
646
708
  )
647
- .join('')}</select
648
- ><label class="amount-field"
709
+ .join('')}
710
+ <option value="custom" ${selection === 'custom' ? 'selected' : ''}>
711
+ Custom amount
712
+ </option></select
713
+ ><label
714
+ class="amount-field billing-amount ${subscribed ? '' : 'hidden'}"
715
+ id="${prefix}amount-${p}"
649
716
  ><span class="sr-only">${name(p)} monthly subscription in USD</span><span>$</span
650
717
  ><input
651
718
  name="monthly-${p}"
@@ -655,13 +722,36 @@ function billingFields(p , firstRun = false) {
655
722
  step="0.01"
656
723
  value="${plan ? plan.monthly.toFixed(2) : (b.monthly ?? '')}"
657
724
  ${plan ? 'readonly' : ''}
658
- ${firstRun && b.mode === 'SUBSCRIPTION' ? 'required' : ''}
725
+ ${!subscribed ? 'disabled' : ''}
726
+ ${firstRun && subscribed ? 'required' : ''}
659
727
  placeholder="Amount"
660
728
  /><span>/ month</span></label
661
729
  >
730
+ <label class="mixed-field ${subscribed ? '' : 'hidden'}"
731
+ ><input
732
+ type="checkbox"
733
+ name="mixed-${p}"
734
+ data-mixed-billing="${p}"
735
+ ${b.mode === 'MIXED' ? 'checked' : ''}
736
+ ${subscribed ? '' : 'disabled'}
737
+ />Subscription + API</label
738
+ >
739
+ ${expenses ? `<label class="api-spend-field ${api ? '' : 'hidden'}"><span>API paid</span><div class="amount-field"><span>$</span><input name="api-spend-${p}" type="number" min="0" max="1000000" step="0.01" value="${b.apiSpend ?? (b.mode === 'MIXED' ? 0 : '')}" ${api ? '' : 'disabled'} placeholder="Not set" aria-label="${name(p)} API paid in USD"/></div></label>` : ''}
662
740
  </div>
663
741
  </div>`;
664
742
  }
743
+ function expenseDialog() {
744
+ if (!expenseProvider) return '';
745
+ const billing = billingForSelection(settings, expenseProvider, summary.range);
746
+ return `<dialog id="expenses-dialog" aria-labelledby="expenses-title">
747
+ <div class="expenses-heading"><h2 id="expenses-title">${name(expenseProvider)} · Expenses</h2><button type="button" class="text-button" id="close-expenses" aria-label="Close expenses">×</button></div>
748
+ <form id="expenses-form">
749
+ <p class="expense-period">${esc(periodLabel())}</p>
750
+ ${billingFields(expenseProvider, true, billing, true, 'period-')}
751
+ <p class="expense-note">This period only · USD</p><p id="expense-error" role="alert" hidden></p>
752
+ <div class="expense-actions"><button type="button" class="text-button" id="reset-expenses">Use default</button><button class="button" type="submit">Save</button></div>
753
+ </form></dialog>`;
754
+ }
665
755
  function settingsPage() {
666
756
  return /* HTML */ `<div class="page-heading">
667
757
  <div><h1>Settings</h1></div>
@@ -734,7 +824,11 @@ function settingsPage() {
734
824
  <h2>Billing</h2>
735
825
  ${(['codex', 'claude'] )
736
826
  .filter((p) => sources.find((s) => s.provider === p)?.detected)
737
- .map((p) => billingFields(p))
827
+ .map(
828
+ (p) =>
829
+ billingFields(p) +
830
+ `<button type="button" class="text-button settings-expenses" data-expenses="${p}">${name(p)} · Monthly expenses</button>`,
831
+ )
738
832
  .join('')}
739
833
  <details class="compact-details">
740
834
  <summary>Details</summary>
@@ -754,8 +848,8 @@ function settingsPage() {
754
848
  </p>
755
849
  <p>
756
850
  These are your declarations, not inferred billing history. Amounts are in USD.
757
- Comparisons use the full monthly price for full calendar months only. API and mixed
758
- usage have no subscription comparison.
851
+ Comparisons use full calendar months. API spend belongs to the selected month; missing
852
+ amounts stay unknown. Value multiple is API equivalent divided by total paid.
759
853
  </p>
760
854
  </details>
761
855
  </section>
@@ -792,7 +886,7 @@ function onboarding() {
792
886
  <span class="brand-mark">${productMark}</span
793
887
  ><span class="eyebrow">${esc(product.name.toUpperCase())} / FIRST RUN</span>
794
888
  <h2 id="onboarding-title">
795
- ${onboardingStage === 'found' ? (found.length ? 'Local usage found' : 'No local usage found') : 'How did you use it?'}
889
+ ${onboardingStage === 'found' ? (found.length ? 'Local usage found' : 'No local usage found') : 'Your subscriptions'}
796
890
  </h2>
797
891
  ${
798
892
  onboardingStage === 'found'
@@ -804,7 +898,7 @@ function onboarding() {
804
898
  ${billable
805
899
  .map(
806
900
  (p) =>
807
- /* HTML */ `<h3>How did you use ${name(p.provider)}?</h3>
901
+ /* HTML */ `<h3>${name(p.provider)}</h3>
808
902
  ${billingFields(p.provider, true)}`,
809
903
  )
810
904
  .join('')}<button class="button" type="submit">
@@ -830,6 +924,14 @@ function render() {
830
924
  </footer>
831
925
  </div>`
832
926
  }`;
927
+ app.insertAdjacentHTML('beforeend', expenseDialog());
928
+ const expense = document.querySelector ('#expenses-dialog');
929
+ if (expense) {
930
+ expense.addEventListener('cancel', () => {
931
+ expenseProvider = null;
932
+ });
933
+ expense.showModal();
934
+ }
833
935
  const canvas = document.querySelector ('#receipt-canvas');
834
936
  if (canvas) drawReceipt(canvas, displayedReceipt, settings.receiptTheme ?? 'dark');
835
937
  const dialog = document.querySelector ('#onboarding');
@@ -839,22 +941,36 @@ function render() {
839
941
  }
840
942
  bind();
841
943
  }
842
- function readBilling(form ) {
944
+ function billingFromForm(form , p ) {
843
945
  const data = new FormData(form);
946
+ const selection = data.get(`plan-${p}`);
947
+ if (selection === null) return null;
948
+ const subscribed = selection !== 'none' && selection !== '',
949
+ mode = subscribed ? (data.has(`mixed-${p}`) ? 'MIXED' : 'SUBSCRIPTION') : 'API',
950
+ plan = subscriptionPlan(p, String(selection)),
951
+ amount = data.get(`monthly-${p}`),
952
+ api = data.get(`api-spend-${p}`);
953
+ return {
954
+ mode,
955
+ monthly: subscribed
956
+ ? (plan?.monthly ?? (amount !== '' && amount !== null ? Number(amount) : null))
957
+ : null,
958
+ ...(subscribed ? { planId: plan?.id ?? null } : {}),
959
+ ...(mode !== 'SUBSCRIPTION'
960
+ ? { apiSpend: api !== '' && api !== null ? Number(api) : null }
961
+ : {}),
962
+ };
963
+ }
964
+ function readBilling(form ) {
844
965
  for (const p of ['codex', 'claude'] ) {
845
- const mode = data.get(`mode-${p}`);
846
- if (mode) {
847
- const amount = data.get(`monthly-${p}`),
848
- plan = subscriptionPlan(p, String(data.get(`plan-${p}`)));
849
- settings.billing[p] = {
850
- mode: mode ,
851
- monthly:
852
- mode === 'SUBSCRIPTION'
853
- ? (plan?.monthly ?? (amount !== '' && amount !== null ? Number(amount) : null))
854
- : null,
855
- ...(mode === 'SUBSCRIPTION' ? { planId: plan?.id ?? null } : {}),
856
- };
857
- }
966
+ const billing = billingFromForm(form, p);
967
+ if (!billing) continue;
968
+ // API payments are entered in the provider card for a specific period.
969
+ settings.billing[p] = {
970
+ mode: billing.mode,
971
+ monthly: billing.monthly,
972
+ ...(billing.planId !== undefined ? { planId: billing.planId } : {}),
973
+ };
858
974
  }
859
975
  }
860
976
  async function save() {
@@ -1011,27 +1127,110 @@ function bind() {
1011
1127
  const data = new FormData(e.currentTarget );
1012
1128
  void choosePeriod('month', from, to, String(data.get('month')));
1013
1129
  });
1014
- document.querySelectorAll ('[data-billing-mode]').forEach((select) =>
1015
- select.addEventListener('change', () => {
1016
- document
1017
- .querySelector(`#amount-${select.dataset.billingMode}`)
1018
- ?.classList.toggle('hidden', select.value !== 'SUBSCRIPTION');
1019
- const amount = document.querySelector (
1020
- `[name="monthly-${select.dataset.billingMode}"]`,
1021
- );
1022
- if (amount)
1023
- amount.required = select.dataset.firstRun === 'true' && select.value === 'SUBSCRIPTION';
1130
+ const updateBillingFields = (row ) => {
1131
+ const select = row.querySelector ('[data-subscription-plan]') ;
1132
+ const p = select.dataset.subscriptionPlan ,
1133
+ plan = subscriptionPlan(p, select.value),
1134
+ input = row.querySelector (`[name="monthly-${p}"]`) ,
1135
+ mixed = row.querySelector ('[data-mixed-billing]') ;
1136
+ const subscribed = select.value !== '' && select.value !== 'none';
1137
+ row.querySelector('.billing-amount')?.classList.toggle('hidden', !subscribed);
1138
+ row.querySelector('.mixed-field')?.classList.toggle('hidden', !subscribed);
1139
+ input.disabled = mixed.disabled = !subscribed;
1140
+ input.required = select.dataset.firstRun === 'true' && subscribed;
1141
+ input.readOnly = !!plan;
1142
+ if (plan) input.value = plan.monthly.toFixed(2);
1143
+ const api = !subscribed || mixed.checked;
1144
+ row.querySelector('.api-spend-field')?.classList.toggle('hidden', !api);
1145
+ const apiInput = row.querySelector (`[name="api-spend-${p}"]`);
1146
+ if (apiInput) {
1147
+ apiInput.disabled = !api;
1148
+ if (mixed.checked && subscribed && apiInput.value === '') apiInput.value = '0';
1149
+ }
1150
+ };
1151
+ document.querySelectorAll ('[data-billing-provider]').forEach((row) => {
1152
+ row
1153
+ .querySelectorAll('select, [data-mixed-billing]')
1154
+ .forEach((field) => field.addEventListener('change', () => updateBillingFields(row)));
1155
+ });
1156
+ document.querySelectorAll ('[data-expenses]').forEach((button) =>
1157
+ button.addEventListener('click', () => {
1158
+ expenseProvider = button.dataset.expenses ;
1159
+ render();
1024
1160
  }),
1025
1161
  );
1026
- document.querySelectorAll ('[data-subscription-plan]').forEach((select) =>
1027
- select.addEventListener('change', () => {
1028
- const p = select.dataset.subscriptionPlan ,
1029
- plan = subscriptionPlan(p, select.value),
1030
- input = document.querySelector (`[name="monthly-${p}"]`) ;
1031
- input.readOnly = !!plan;
1032
- if (plan) input.value = plan.monthly.toFixed(2);
1162
+ document.querySelectorAll ('[data-expense-toggle]').forEach((button) => {
1163
+ const panel = document.getElementById(button.getAttribute('aria-controls') ) ;
1164
+ panel.addEventListener('toggle', () =>
1165
+ button.setAttribute('aria-expanded', String(panel.matches(':popover-open'))),
1166
+ );
1167
+ button.addEventListener('click', () => {
1168
+ if (panel.matches(':popover-open')) {
1169
+ panel.hidePopover();
1170
+ return;
1171
+ }
1172
+ panel.showPopover();
1173
+ const bounds = button.getBoundingClientRect();
1174
+ panel.style.left =
1175
+ Math.max(
1176
+ 16,
1177
+ Math.min(innerWidth - panel.offsetWidth - 16, bounds.right - panel.offsetWidth),
1178
+ ) + 'px';
1179
+ panel.style.top =
1180
+ Math.max(16, Math.min(innerHeight - panel.offsetHeight - 16, bounds.bottom + 8)) + 'px';
1181
+ panel.querySelector ('select')?.focus();
1182
+ });
1183
+ });
1184
+ document.querySelector('#close-expenses')?.addEventListener('click', () => {
1185
+ expenseProvider = null;
1186
+ render();
1187
+ });
1188
+ const persistExpenses = async (provider , form , reset = false) => {
1189
+ if (expensesSaving) return;
1190
+ expensesSaving = true;
1191
+ const billing = billingFromForm(form, provider),
1192
+ previous = settings,
1193
+ next = withPeriodBilling(settings, provider, summary.range, reset ? null : billing);
1194
+ document
1195
+ .querySelectorAll ('[data-period-billing] button, #expenses-form button')
1196
+ .forEach((button) => (button.disabled = true));
1197
+ try {
1198
+ await api('/api/settings', 'POST', next);
1199
+ settings = next;
1200
+ await load();
1201
+ expenseProvider = null;
1202
+ expensesSaving = false;
1203
+ render();
1204
+ toast('Saved locally');
1205
+ } catch {
1206
+ settings = previous;
1207
+ expensesSaving = false;
1208
+ const error = form.querySelector ('.billing-error, #expense-error') ;
1209
+ error.hidden = false;
1210
+ error.textContent = 'Unable to save expenses';
1211
+ document
1212
+ .querySelectorAll ('[data-period-billing] button, #expenses-form button')
1213
+ .forEach((button) => (button.disabled = false));
1214
+ }
1215
+ };
1216
+ document.querySelectorAll ('[data-period-billing]').forEach((form) =>
1217
+ form.addEventListener('submit', (e) => {
1218
+ e.preventDefault();
1219
+ void persistExpenses(form.dataset.periodBilling , form);
1033
1220
  }),
1034
1221
  );
1222
+ document.querySelector ('#expenses-form')?.addEventListener('submit', (e) => {
1223
+ e.preventDefault();
1224
+ if (expenseProvider) void persistExpenses(expenseProvider, e.currentTarget );
1225
+ });
1226
+ document.querySelector('#reset-expenses')?.addEventListener('click', () => {
1227
+ if (expenseProvider)
1228
+ void persistExpenses(
1229
+ expenseProvider,
1230
+ document.querySelector ('#expenses-form') ,
1231
+ true,
1232
+ );
1233
+ });
1035
1234
  document
1036
1235
  .querySelector ('#settings-form')
1037
1236
  ?.addEventListener('submit', async (e) => {