billing/dev: Vertragsende in der MAN-Abrechnung, Rechnungslauf ohne Tageskappe, Qualitätsgates (Audit Block 5+6, Deploy E)
Block 5 — MAN-Abrechnung F06 (E6): Ein explizites Legacy-Vertragsende (grandfathered_until) deckelt jetzt Auswahl und Sperr-Nachprüfung: Beginnt die nächste Periode nach dem Vertragsende, gibt es keine Rechnung mehr. Neuer täglicher Befehl billing:expire-grandfathering (04:15, vor dem Rechnungslauf) setzt abgelaufene Posten auf gekündigt und schickt je Kunde eine Mail (GrandfatheringExpired). Auf der Dev-Kopie trägt kein Posten ein Vertragsende; der Fehler war latent. F16: Nicht abrechenbare Vereinbarungen bekommen einen Sperrvermerk (billing_hold_reason/-at), sichtbar auf der Admin-Zahlungsseite, und stehen im Rechnungslauf hinten an. Der Lauf arbeitet den fälligen Bestand in Seiten ab; --limit deckelt die erstellten Rechnungen (500), nicht die betrachteten Fälle. Vorher blockierten 50 unvollständige Fälle die 51. dauerhaft, und der Durchsatz war auf 50 am Tag begrenzt. Block 6 — Entwicklung F24: Lint-Workflow prüft mit pint --test statt zu formatieren; keine Schreibrechte mehr. F27: composer run dev startet beide Vite-Server (npm run dev:all). F28: docs/README.md als historischer Stand markiert, toter Link auf das Hub-Flux-Archiv korrigiert, aktueller Einstieg dev/README.md. F26: Login-Modal übernimmt den Fokus, hält Tab im Dialog und gibt den Fokus beim Schließen an den Auslöser zurück – ohne neue Abhängigkeit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
72432fdb82
commit
9a88e82baa
15 changed files with 518 additions and 65 deletions
|
|
@ -100,7 +100,35 @@
|
|||
|
||||
{{-- Header (Logo + Suche mittig + Buttons rechtsbündig + Burger) --}}
|
||||
<div id="header" class="header-normal bg-bg border-b border-bg-rule"
|
||||
x-data="{ searchOpen: false, loginOpen: false }"
|
||||
x-data="{
|
||||
searchOpen: false,
|
||||
loginOpen: false,
|
||||
loginTrigger: null,
|
||||
// Fokusführung des eigenen Dialogs (Audit F26): Fokus beim Öffnen
|
||||
// ins Modal, Tab bleibt darin, beim Schließen zurück zum Auslöser.
|
||||
loginFocusables() {
|
||||
return Array.from(this.$refs.loginDialog?.querySelectorAll('a[href], button:not([disabled]), input, [tabindex]:not([tabindex=\'-1\'])') ?? [])
|
||||
.filter(el => el.offsetParent !== null);
|
||||
},
|
||||
trapLoginTab(event) {
|
||||
const items = this.loginFocusables();
|
||||
if (items.length === 0) { return; }
|
||||
const first = items[0], last = items[items.length - 1];
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
||||
else if (! event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||
},
|
||||
init() {
|
||||
this.$watch('loginOpen', open => {
|
||||
if (open) {
|
||||
this.loginTrigger = document.activeElement;
|
||||
this.$nextTick(() => (this.$refs.loginPrimary ?? this.loginFocusables()[0])?.focus());
|
||||
} else if (this.loginTrigger instanceof HTMLElement) {
|
||||
this.loginTrigger.focus();
|
||||
this.loginTrigger = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}"
|
||||
@keydown.escape.window="searchOpen = false; loginOpen = false"
|
||||
@keydown.window.prevent.cmd.k="$refs.desktopSearch ? $refs.desktopSearch.focus() : searchOpen = true"
|
||||
@keydown.window.prevent.ctrl.k="$refs.desktopSearch ? $refs.desktopSearch.focus() : searchOpen = true">
|
||||
|
|
@ -191,11 +219,13 @@
|
|||
x-transition.opacity.duration.150ms
|
||||
class="fixed inset-0 z-50 bg-ink/70 backdrop-blur-sm flex items-start sm:items-center justify-center p-4 sm:p-6 overflow-y-auto"
|
||||
@click.self="loginOpen = false"
|
||||
@keydown.tab="trapLoginTab($event)"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="login-modal-title"
|
||||
style="display: none;">
|
||||
<div x-show="loginOpen"
|
||||
x-ref="loginDialog"
|
||||
x-transition.duration.200ms
|
||||
class="relative w-full max-w-[520px] bg-bg border border-bg-rule-strong shadow-2xl mt-10 sm:mt-0">
|
||||
<button type="button"
|
||||
|
|
@ -218,6 +248,7 @@
|
|||
|
||||
<div class="flex flex-col sm:flex-row gap-2.5">
|
||||
<a href="{{ $portalUrl }}/login"
|
||||
x-ref="loginPrimary"
|
||||
class="inline-flex items-center justify-center gap-2 px-[18px] py-2.5 text-[13px] font-semibold text-white bg-brand cursor-pointer hover:bg-brand-deep rounded-[2px] transition-colors">
|
||||
{{ __('web.header.login_modal_login') }}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<x-emails.layout title="Ihr Bestandsschutz ist ausgelaufen" preheader="Ihr Bestandsschutz auf pressekonto ist zum vereinbarten Vertragsende ausgelaufen.">
|
||||
<p>Guten Tag {{ $user->name }},</p>
|
||||
|
||||
<p>
|
||||
Ihr Bestandsschutz aus dem früheren Vertrag ist zum vereinbarten Ende
|
||||
@if ($endedOn)
|
||||
am <strong>{{ $endedOn->format('d.m.Y') }}</strong>
|
||||
@endif
|
||||
ausgelaufen. Es wird keine weitere Rechnung mehr gestellt.
|
||||
</p>
|
||||
|
||||
<dl class="meta">
|
||||
<dt>Bestandsschutz</dt>
|
||||
<dd>beendet{{ $endedOn ? ' zum '.$endedOn->format('d.m.Y') : '' }}</dd>
|
||||
</dl>
|
||||
|
||||
<p>
|
||||
Ihre bereits veröffentlichten Pressemitteilungen bleiben unverändert online.
|
||||
Für neue Veröffentlichungen stehen Ihnen die aktuellen Tarife und der
|
||||
Einzelversand auf pressekonto zur Verfügung — jederzeit direkt in Ihrem Konto buchbar.
|
||||
</p>
|
||||
|
||||
<x-emails.button :url="$bookingUrl">Tarife ansehen</x-emails.button>
|
||||
|
||||
<div class="note">
|
||||
<p>Bei Fragen erreichen Sie uns über den <a href="{{ config('domains.domain_portal_url') }}/de/hilfe">Hilfe-Bereich auf pressekonto</a>.</p>
|
||||
</div>
|
||||
</x-emails.layout>
|
||||
|
|
@ -475,10 +475,13 @@ new #[Layout('components.layouts.app'), Title('Zahlungen')] class extends Compon
|
|||
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $blocked->current_period_end?->diffForHumans() }}</div>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
@if (! $blocked->user?->billingAddress)
|
||||
@if ($blocked->billing_hold_reason)
|
||||
<span class="badge warn dot">{{ $blocked->billing_hold_reason }}</span>
|
||||
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('seit :date', ['date' => $blocked->billing_hold_at?->format('d.m.Y')]) }}</div>
|
||||
@elseif (! $blocked->user?->billingAddress)
|
||||
<span class="badge warn dot">{{ __('Keine Rechnungsadresse') }}</span>
|
||||
@else
|
||||
<span class="badge">{{ __('Siehe Log') }}</span>
|
||||
<span class="badge">{{ __('Noch nicht geprüft') }}</span>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue