Files
pw/src/pages/profiler.astro
T
Asger Geel Weirsøe a352b32fac
Build & Deploy / deploy (push) Successful in 2m41s
Use the theme's chip and tag components for every pill on the site
Links are .wt-chip, labels are .wt-tag, highlighted labels .wt-tag--accent.
Bumps Weircon-wind for the status tag variants.

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
2026-10-29 18:41:00 +01:00

208 lines
9.1 KiB
Plaintext

---
import Base from '../layouts/Base.astro';
import { profiles, standarder, kategorier } from '../data/profiles';
const searchString = (p: (typeof profiles)[number]) =>
[p.standard, p.kategori ?? '', p.navn, p.id, p.customization, p.dokumenter.join(' '), p.anvendelse, p.bemaerkninger]
.join(' ')
.toLowerCase();
// Registerprofilerne af "single"-typen står ikke i G26, dem henviser vi til registret for.
const kunIRegistret = new Set(['Procurement-OrdRes-1.0', 'Procurement-BilSimReminderOnly-1.0']);
const kilde = (p: (typeof profiles)[number]) => {
if (kunIRegistret.has(p.id))
return { label: 'Nemhandelsregistret', url: 'https://nemhandel.dk/nemhandelsregisteret' };
if (p.standard.startsWith('Nemhandel'))
return { label: 'OIOUBL Profiler (G26)', url: 'https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf' };
if (p.standard.includes('PINT'))
return p.navn.includes('(kommende)')
? { label: 'Peppol roadmap', url: 'https://docs.peppol.eu/poac/' }
: { label: 'docs.peppol.eu \u00b7 EU PINT Billing', url: 'https://docs.peppol.eu/poac/eu/pint-eu/' };
if (p.navn.includes('Self-Billing'))
return { label: 'docs.peppol.eu \u00b7 Self-Billing 3.0', url: 'https://docs.peppol.eu/poacc/self-billing/3.0/' };
if (p.navn.includes('Billing 3.0'))
return { label: 'docs.peppol.eu \u00b7 BIS Billing 3.0', url: 'https://docs.peppol.eu/poacc/billing/3.0/' };
return { label: 'docs.peppol.eu \u00b7 BIS v3', url: 'https://docs.peppol.eu/poacc/upgrade-3/' };
};
---
<Base
title="Profil-opslag (Nemhandel & Peppol)"
description="Søgbart opslag over Nemhandel OIOUBL- og Peppol BIS/PINT-profiler: profil-id'er, customization-id'er, indeholdte dokumenter og typisk anvendelse."
>
<h1 class="text-4xl font-bold tracking-tight">Profil-opslag</h1>
<p class="mt-4 max-w-2xl text-muted">
Browse og søg i e-dokumentprofilerne for <strong class="text-text">Nemhandel OIOUBL</strong> og
<strong class="text-text">Peppol BIS/PINT</strong>: profil-/proces-id, customization-id, hvilke
dokumenter profilen indeholder og typisk anvendelse. Klik på et id for at kopiere det.
</p>
<p class="mt-3 max-w-2xl text-sm text-muted">
De danske profiler står i samme rækkefølge og under samme gruppenavne som i
<strong class="text-text">Nemhandelsregistrets</strong> profil-dropdown (“Profiler for: …”), så du kan
slå direkte op mellem registrering og dokumentindhold.
</p>
<div class="mt-8 flex flex-col gap-4 sm:flex-row sm:items-center">
<div class="relative flex-1">
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted">⌕</span>
<input
id="p-search"
type="search"
placeholder="Søg profil, id, dokument…"
autocomplete="off"
aria-label="Søg profiler"
class="w-full rounded-xl border border-border bg-surface/60 py-2.5 pl-9 pr-4 outline-none focus:border-accent"
/>
</div>
<span id="p-count" class="text-sm text-muted">{profiles.length} profiler</span>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button type="button" class="p-chip" data-std="" aria-pressed="true">Alle</button>
{standarder.map((s) => <button type="button" class="p-chip" data-std={s} aria-pressed="false">{s}</button>)}
</div>
<div class="mt-3">
<span class="mr-2 text-xs text-muted">Nemhandelsregistrets kategori:</span>
<span class="mt-1 inline-flex flex-wrap gap-2 align-middle">
<button type="button" class="p-chip" data-kat="" aria-pressed="true">Alle</button>
{kategorier.map((k) => <button type="button" class="p-chip" data-kat={k} aria-pressed="false">{k}</button>)}
</span>
</div>
<div class="mt-6 space-y-3">
{
profiles.map((p) => { const k = kilde(p); return (
<article
data-profile
data-standard={p.standard}
data-kategori={p.kategori ?? ''}
data-search={searchString(p)}
class="rounded-2xl border border-border bg-surface/60 p-5 transition-colors hover:border-accent/50"
>
<div class="flex flex-wrap items-center gap-2">
<span class="wt-tag wt-tag--accent">
{p.standard}
</span>
{p.kategori && (
<span
class="wt-tag wt-muted"
title="Gruppen profilen står under i Nemhandelsregistrets dropdown"
>
Register: {p.kategori}
</span>
)}
</div>
<h2 class="mt-2 text-lg font-semibold tracking-tight">{p.navn}</h2>
<dl class="mt-3 grid gap-1 text-xs sm:grid-cols-[10rem_1fr]">
<dt class="text-muted">Profil/proces-id</dt>
<dd>
<button type="button" data-copy={p.id} title="Klik for at kopiere" class="copy break-all text-left font-mono">
{p.id}
</button>
</dd>
<dt class="text-muted">Customization-id</dt>
<dd>
<button type="button" data-copy={p.customization} title="Klik for at kopiere" class="copy break-all text-left font-mono">
{p.customization}
</button>
</dd>
</dl>
<div class="mt-3 flex flex-wrap gap-1.5">
{p.dokumenter.map((d) => (
<span class="wt-tag">{d}</span>
))}
</div>
<p class="mt-3 text-sm text-muted">{p.anvendelse}</p>
<p class="mt-2 text-xs text-muted">
<span class="font-semibold text-muted">Bemærk:</span> {p.bemaerkninger}
</p>
<p class="mt-3 text-xs"><a href={k.url} target="_blank" rel="noopener" class="text-accent/80 transition-colors hover:text-accent">Kilde: {k.label} ↗</a></p>
</article>
); })
}
</div>
<p id="p-none" class="mt-6 text-sm text-muted" hidden>Ingen profiler matcher.</p>
<p class="mt-10 text-xs text-muted">
Kilder: OIOUBL Profiler-guiden (G26) v1.3 (dokumentlister, tabel 1-3), Nemhandelsregistret
(gruppering og profilnavne) og Peppol BIS/PINT (docs.peppol.eu). * = ApplicationResponse bruges kun
til afvisning; det forretningsmæssige svar på en ordre er altid OrderResponse/OrderResponseSimple.
Øjebliksbillede.. verificér altid id'er mod den officielle dokumentation før brug.
</p>
<style is:inline>
.p-chip {
border: 1px solid var(--color-border);
color: var(--color-muted);
border-radius: 0.6rem;
padding: 0.35rem 0.8rem;
font-size: 0.85rem;
cursor: pointer;
background: transparent;
transition: color 0.15s, background-color 0.15s, border-color 0.15s;
}
.p-chip:hover { color: var(--color-text); }
.p-chip[aria-pressed="true"] {
background: var(--color-surface-2);
color: var(--color-text);
border-color: color-mix(in srgb, var(--color-accent) 40%, var(--color-border));
}
.copy { transition: color 0.15s; }
.copy:hover { color: var(--color-accent); text-decoration: underline; text-underline-offset: 3px; }
</style>
<script is:inline>
(function () {
var cards = [].slice.call(document.querySelectorAll('[data-profile]'));
var search = document.getElementById('p-search');
var stdChips = [].slice.call(document.querySelectorAll('.p-chip[data-std]'));
var katChips = [].slice.call(document.querySelectorAll('.p-chip[data-kat]'));
var count = document.getElementById('p-count');
var none = document.getElementById('p-none');
var q = '', std = '', kat = '';
function apply() {
var shown = 0;
cards.forEach(function (c) {
var okS = !std || c.getAttribute('data-standard') === std;
var okK = !kat || c.getAttribute('data-kategori') === kat;
var okQ = !q || c.getAttribute('data-search').indexOf(q) > -1;
var hit = okS && okK && okQ;
c.hidden = !hit;
if (hit) shown++;
});
count.textContent = shown + (shown === 1 ? ' profil' : ' profiler');
none.hidden = shown > 0;
}
search.addEventListener('input', function () { q = search.value.trim().toLowerCase(); apply(); });
function wire(group, attr, set) {
group.forEach(function (b) {
b.addEventListener('click', function () {
set(b.getAttribute(attr) || '');
group.forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false'); });
apply();
});
});
}
wire(stdChips, 'data-std', function (v) { std = v; });
wire(katChips, 'data-kat', function (v) { kat = v; });
document.addEventListener('click', function (e) {
var btn = e.target.closest && e.target.closest('[data-copy]');
if (!btn || !navigator.clipboard) return;
navigator.clipboard.writeText(btn.getAttribute('data-copy')).then(function () {
var orig = btn.textContent;
btn.textContent = 'kopieret ✓';
setTimeout(function () { btn.textContent = orig; }, 1200);
}).catch(function () {});
});
var qp = new URLSearchParams(location.search).get('q');
if (qp) { search.value = qp; q = qp.trim().toLowerCase(); }
apply();
})();
</script>
</Base>