PageSourceSearch

https://invoicecraftly.com/_assets/js/features/localization/norway-editor-foundation.a5ea0b1e812d.js

js invoicecraftly.com collected 2026-09-24 14:45:57 UTC 12,292 bytes, 225 lines download raw bytes

1import { getMarketInvoiceNumberingPolicy } from '../invoice-numbering/market-numbering-policy.f4f4cec91dfe.js';
2import { makulerNorwegianInvoice } from '../invoice-numbering/regulated-document-void-service.1c519d02736b.js';
3import { getStorageAdapter } from '../invoice-storage/storage-adapter.b2ee2abd8251.js';
4import { getEditorInvoice, onEditorRendered } from '../../app/main.599ec0e5abb9.js';
5import { appendPolicyNote, applyReadonlyNumberField } from '../editor-controls/editor-field-primitives.87e6260d0551.js';
6
7const MARKET_ID = 'norway';
8const NUMBER_NOTE_ID = 'norway-numbering-policy-note';
9const PREFIX_NOTE_ID = 'norway-number-prefix-policy-note';
10// Slice 6 of the editor-kernel post-#126 hardening skill: `storage` is kept only for
11// makulerNorwegianInvoice() below, a domain write triggered by an explicit user action (voiding an
12// invoice) — a legitimate direct storage use, not the private read-hydration this slice removes.
13const storage = getStorageAdapter();
14
15function protectSystemAssignedNumbering(root = document) {
16  const policy = getMarketInvoiceNumberingPolicy(MARKET_ID);
17  if (policy.mode !== 'system-controlled') return;
18
19  const numberControls = [];
20  if (root.nodeType === Node.ELEMENT_NODE && root.matches?.('[data-path="invoiceNumber"]')) numberControls.push(root);
21  numberControls.push(...(root.querySelectorAll?.('[data-path="invoiceNumber"]') || []));
22  numberControls.forEach((control) => applyReadonlyNumberField(control, {
23    noteId: NUMBER_NOTE_ID,
24    noteText: 'Fakturanummeret tildeles maskinelt fra virksomhetens lokale nummerserie og kan ikke overstyres i editoren.'
25  }));
26
27  const prefixControls = [];
28  if (root.nodeType === Node.ELEMENT_NODE && root.matches?.('[data-path="invoiceNumberPrefix"]')) prefixControls.push(root);
29  prefixControls.push(...(root.querySelectorAll?.('[data-path="invoiceNumberPrefix"]') || []));
30  prefixControls.forEach((control) => applyReadonlyNumberField(control, {
31    noteId: PREFIX_NOTE_ID,
32    noteText: 'Nummerformatet er låst for den norske nummerserien.'
33  }));
34}
35
36// Slice 5 of the editor-kernel post-#126 hardening skill: the kernel now always renders
37// seller.taxId as the always-required, correctly labeled/placeholder'd/formatted organization-number
38// field directly (country-profiles.js's editorFields.requiredSellerTaxId, consumed by main.js's
39// optionalTaxIdHTML() override) — this function's former field creation, relabeling, button hiding,
40// and repositioning are all gone. All that remains is attaching the policy note the kernel doesn't
41// know how to phrase.
42function ensureSellerOrganizationNumberNote() {
43  const control = document.querySelector('[data-validation-section="seller"] [data-path="seller.taxId"]');
44  if (control) {
45    appendPolicyNote(
46      control,
47      'norway-organization-number-note',
48      'InvoiceCraftly kontrollerer formatet, men verifiserer ikke registreringen mot Brønnøysundregistrene.'
49    );
50  }
51}
52
53// Slice 5 of the editor-kernel post-#126 hardening skill (company-identity step): this used to build/
54// insert the legalForm/businessRegisterStatus/liquidationStatus selects, toggle the latter two's
55// visibility, and resolve the shared note's text — all three are now kernel-rendered directly from
56// country-profiles.js's editorFields.seller (visibleWhen/notes). The former updateSellerAddressLabel()
57// (address-label step) — relabeling the kernel's own seller address field based on legalForm — is now
58// the same profile's editorFields.sellerAddressLabel, resolved by main.js's
59// resolveSellerAddressLabelKey() at render time and kept live by applyConditionalFieldVisibility().
60// Nothing legalForm-dependent remains in this file.
61
62// Tax-reporting step (editor-kernel post-#126 hardening skill): this used to build/insert the
63// exchange-rate/rate-date/rate-source group, compute its own foreignCurrencyVatSelected() visibility
64// condition, and rewrite its dynamic label by hand — all now kernel-rendered directly from
65// country-profiles.js's editorFields.taxReporting (visibleWhen's AND-composed conditions plus
66// labelParams). Nothing currency/vatStatus-dependent remains in this file.
67
68function updateMakulerActions(invoice = getEditorInvoice()) {
69  const currentAction = document.getElementById('more-remove-local-copy');
70  const status = invoice?.metadata?.regulatedNumbering?.status || null;
71  if (currentAction) {
72    const label = currentAction.querySelector('span');
73    currentAction.dataset.norwayMakulerAction = 'true';
74    if (!invoice || invoice.localization?.marketId !== MARKET_ID) {
75      currentAction.disabled = true;
76      currentAction.title = 'Norsk dokumentstatus lastes.';
77      if (label) label.textContent = 'Makuler faktura';
78    } else if (status === 'issued' || invoice.metadata?.lastExportedAt) {
79      currentAction.disabled = true;
80      currentAction.title = 'En utsendt faktura skal korrigeres med kreditnota, ikke makuleres.';
81      if (label) label.textContent = 'Bruk kreditnota';
82    } else if (status === 'void' || invoice.lifecycle?.document === 'voided') {
83      currentAction.disabled = true;
84      currentAction.title = 'Denne fakturaen er allerede makulert og nummeret beholdes i nummerserien.';
85      if (label) label.textContent = 'Makulert';
86    } else {
87      currentAction.disabled = false;
88      currentAction.removeAttribute('aria-disabled');
89      currentAction.title = 'Makuler en ikke-utsendt faktura og behold nummeret i nummerserien';
90      if (label) label.textContent = 'Makuler faktura';
91    }
92  }
93
94  const explanation = 'Åpne fakturaen først for å bruke Makuler. Nummeret og historikken skal beholdes.';
95  document.querySelectorAll('[data-recent-action="delete"]').forEach((button) => {
96    button.disabled = true;
97    button.setAttribute('aria-disabled', 'true');
98    button.title = explanation;
99    button.dataset.norwayNumberingProtected = 'true';
100    const label = button.querySelector('span');
101    if (label && label.textContent !== 'Åpne for å makulere') label.textContent = 'Åpne for å makulere';
102  });
103}
104
105function localizeDeliveryPreview() {
106  document.querySelectorAll('.pv-delivery strong').forEach((label) => {
107    if (/^Delivery:/i.test(label.textContent || '')) label.textContent = 'Levering:';
108    if (/^Place:/i.test(label.textContent || '')) label.textContent = 'Leveringssted:';
109  });
110}
111
112function applyNorwayFoundation(root = document) {
113  protectSystemAssignedNumbering(root);
114  ensureSellerOrganizationNumberNote();
115  updateMakulerActions();
116  localizeDeliveryPreview();
117}
118
119function closeFoundationModal(returnTarget) {
120  const modalRoot = document.getElementById('modal-root');
121  if (modalRoot) modalRoot.innerHTML = '';
122  returnTarget?.focus?.({ preventScroll: true });
123}
124
125function openMakulerDialog(returnTarget) {
126  const modalRoot = document.getElementById('modal-root');
127  const invoice = getEditorInvoice();
128  const documentId = invoice?.id;
129  if (!modalRoot || !invoice || invoice.localization?.marketId !== MARKET_ID) return;
130
131  if (invoice.metadata?.lastExportedAt || invoice.metadata?.regulatedNumbering?.status === 'issued') {
132    modalRoot.innerHTML = `
133      <div class="modal-backdrop" data-no-makuler-backdrop>
134        <div class="modal" role="dialog" aria-modal="true" aria-labelledby="no-makuler-title">
135          <h3 id="no-makuler-title">Bruk kreditnota</h3>
136          <p>Denne fakturaen er allerede registrert som utsendt. En utsendt faktura skal ikke makuleres i InvoiceCraftly.</p>
137          <button class="tb-btn primary" type="button" data-no-makuler-close>Lukk</button>
138        </div>
139      </div>`;
140    modalRoot.querySelector('[data-no-makuler-close]')?.addEventListener('click', () => closeFoundationModal(returnTarget));
141    return;
142  }
143
144  modalRoot.innerHTML = `
145    <div class="modal-backdrop" data-no-makuler-backdrop>
146      <div class="modal" role="dialog" aria-modal="true" aria-labelledby="no-makuler-title" aria-describedby="no-makuler-description">
147        <button type="button" class="modal-close" data-no-makuler-close aria-label="Lukk">×</button>
148        <span class="modal-kicker">Norsk nummerserie</span>
149        <h3 id="no-makuler-title">Makuler ${invoice.invoiceNumber || 'fakturaen'}?</h3>
150        <p id="no-makuler-description">Nummeret beholdes permanent i nummerserien og dokumentet markeres som makulert. Bruk dette bare for en faktura som ikke er sendt ut.</p>
151        <div class="field">
152          <label for="no-makuler-reason">Årsak</label>
153          <textarea id="no-makuler-reason" rows="3" placeholder="For eksempel feil kunde eller opprettet ved en feil"></textarea>
154        </div>
155        <p class="market-policy-note" data-no-makuler-error role="alert"></p>
156        <div class="business-defaults-actions">
157          <button type="button" class="tb-btn secondary-action" data-no-makuler-close>Avbryt</button>
158          <button type="button" class="tb-btn primary" data-no-makuler-confirm>Makuler og behold nummeret</button>
159        </div>
160      </div>
161    </div>`;
162
163  modalRoot.querySelectorAll('[data-no-makuler-close]').forEach((button) => button.addEventListener('click', () => closeFoundationModal(returnTarget)));
164  modalRoot.querySelector('[data-no-makuler-backdrop]')?.addEventListener('click', (event) => {
165    if (event.target === event.currentTarget) closeFoundationModal(returnTarget);
166  });
167  const reason = modalRoot.querySelector('#no-makuler-reason');
168  const confirm = modalRoot.querySelector('[data-no-makuler-confirm]');
169  const error = modalRoot.querySelector('[data-no-makuler-error]');
170  confirm?.addEventListener('click', async () => {
171    const value = reason?.value?.trim() || '';
172    if (!value) {
173      if (error) error.textContent = 'Skriv en kort årsak før fakturaen makuleres.';
174      reason?.focus();
175      return;
176    }
177    confirm.disabled = true;
178    try {
179      await makulerNorwegianInvoice(storage, { documentId, reason: value });
180      window.location.assign('/no/editor/?type=invoice');
181    } catch (voidError) {
182      confirm.disabled = false;
183      if (error) error.textContent = voidError?.message || 'Kunne ikke makulere fakturaen. Prøv igjen.';
184    }
185  });
186}
187
188// Slice L: dropped this function's own lang/title/meta-description/canonical/dataset.marketId
189// assignments — app.js always calls installNorwayEditorFoundation() before
190// installNorwayEditorPresentation() in the same synchronous install() closure, so presentation's
191// own copies of these same five assignments were already overwriting these ones immediately,
192// every time, with no code running in between to observe the intermediate values. Confirmed dead,
193// not just redundant; removing them changes no behavior. dataset.feature84dNumbering has no
194// equivalent in presentation.js and stays.
195export function installNorwayEditorFoundation() {
196  document.body.dataset.feature84dNumbering = 'foundation';
197
198  applyNorwayFoundation(document);
199
200  document.addEventListener('beforeinput', (event) => {
201    if (event.target?.matches?.('[data-path="invoiceNumber"], [data-path="invoiceNumberPrefix"]')) {
202      event.preventDefault();
203    }
204  }, true);
205
206  document.addEventListener('click', (event) => {
207    const action = event.target.closest?.('#more-remove-local-copy[data-norway-makuler-action="true"]');
208    if (!action || action.disabled) return;
209    event.preventDefault();
210    event.stopImmediatePropagation();
211    openMakulerDialog(action);
212  }, true);
213
214  // Tax-reporting step: dropped the input/change listeners that used to re-run applyNorwayFoundation()
215  // on seller.registration.vatStatus/currency changes — nothing left in applyNorwayFoundation() depends
216  // on either path any more (that was solely ensureTaxReportingFields(), now fully declarative). The
217  // kernel's own applyConditionalFieldVisibility() (main.js) keeps the tax-reporting group's visibility
218  // and dynamic label in sync directly.
219
220  // Slice 6 of the editor-kernel post-#126 hardening skill: replaces the body-wide MutationObserver
221  // that used to re-run applyNorwayFoundation() on every element addition anywhere in <body> (a Rule 4
222  // violation) — main.js's onEditorRendered() fires this exact callback once, synchronously, right
223  // after every renderEditor() pass, the one event that actually matters here.
224  onEditorRendered(() => applyNorwayFoundation(document));
225}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.