Zeitzone Europe/Berlin: unnoetige UTC-Konvertierung entfernt

Beim Speichern/Veroeffentlichen waren Termine 2 Stunden versetzt: die
App-Zeitzone stand auf UTC, Termine wurden nach UTC umgerechnet, aber
published_at roh angezeigt.

- config/app.php: timezone UTC -> Europe/Berlin
- PressRelease: DISPLAY_TIMEZONE + setTimezone-Umrechnung entfernt
- Admin-/Customer-Formulare: scheduledAtUtc() -> resolveScheduledAt(),
  parst direkt in App-Zeitzone; Edit-Hydration ohne setTimezone
- ProofPdfRenderer + Boost-Anzeige: setTimezone entfernt
- Tests an verschiebungsfreies Verhalten angepasst

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-18 12:40:19 +00:00
parent 028b059975
commit 5feaa8a36a
12 changed files with 56 additions and 68 deletions

View file

@ -220,7 +220,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
// Termin wird in Europe/Berlin erfasst; deshalb hier zeitzonen-
// bewusst prüfen statt über die naive `after:`-Regel.
function (string $attribute, mixed $value, \Closure $fail): void {
$scheduledAt = $this->scheduledAtUtc();
$scheduledAt = $this->resolveScheduledAt();
if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) {
$fail(__('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.'));
@ -264,16 +264,16 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
}
/**
* Wandelt den in Europe/Berlin erfassten Termin in den UTC-Zeitpunkt,
* wie er in der Datenbank gespeichert wird. Null, wenn kein Termin gesetzt.
* Parst den erfassten Termin in der Anwendungs-Zeitzone (Europe/Berlin).
* Null, wenn kein Termin gesetzt ist.
*/
protected function scheduledAtUtc(): ?\Carbon\Carbon
protected function resolveScheduledAt(): ?\Carbon\Carbon
{
if (blank($this->scheduledAt)) {
return null;
}
return \Carbon\Carbon::parse($this->scheduledAt, PressRelease::DISPLAY_TIMEZONE)->utc();
return \Carbon\Carbon::parse($this->scheduledAt);
}
protected function validateScheduledAtWhenReady(): void
@ -284,7 +284,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
$this->resetErrorBag('scheduledAt');
$scheduledAt = $this->scheduledAtUtc();
$scheduledAt = $this->resolveScheduledAt();
if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) {
$this->addError('scheduledAt', __('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.'));
@ -567,7 +567,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
'keywords' => $this->keywords ?: null,
'backlink_url' => $this->backlinkUrl ?: null,
'scheduled_at' => $this->publishMode === 'scheduled'
? $this->scheduledAtUtc()
? $this->resolveScheduledAt()
: null,
'embargo_at' => null,
];

View file

@ -108,8 +108,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
?? $this->defaultContactIdFor((int) $pr->company_id);
if ($pr->scheduled_at) {
// DB-Wert ist UTC; für die Eingabefelder nach Europe/Berlin wandeln.
$scheduledAt = $pr->scheduled_at->copy()->setTimezone(PressRelease::DISPLAY_TIMEZONE);
$scheduledAt = $pr->scheduled_at;
$this->publishMode = 'scheduled';
$this->scheduledAt = $scheduledAt->format('Y-m-d\TH:i');
$this->scheduledDate = $scheduledAt->format('Y-m-d');
@ -289,7 +288,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
// Termin wird in Europe/Berlin erfasst; deshalb hier zeitzonen-
// bewusst prüfen statt über die naive `after:`-Regel.
function (string $attribute, mixed $value, \Closure $fail): void {
$scheduledAt = $this->scheduledAtUtc();
$scheduledAt = $this->resolveScheduledAt();
if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) {
$fail(__('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.'));
@ -333,16 +332,16 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
}
/**
* Wandelt den in Europe/Berlin erfassten Termin in den UTC-Zeitpunkt,
* wie er in der Datenbank gespeichert wird. Null, wenn kein Termin gesetzt.
* Parst den erfassten Termin in der Anwendungs-Zeitzone (Europe/Berlin).
* Null, wenn kein Termin gesetzt ist.
*/
protected function scheduledAtUtc(): ?\Carbon\Carbon
protected function resolveScheduledAt(): ?\Carbon\Carbon
{
if (blank($this->scheduledAt)) {
return null;
}
return \Carbon\Carbon::parse($this->scheduledAt, PressRelease::DISPLAY_TIMEZONE)->utc();
return \Carbon\Carbon::parse($this->scheduledAt);
}
protected function validateScheduledAtWhenReady(): void
@ -353,7 +352,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
$this->resetErrorBag('scheduledAt');
$scheduledAt = $this->scheduledAtUtc();
$scheduledAt = $this->resolveScheduledAt();
if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) {
$this->addError('scheduledAt', __('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.'));
@ -435,7 +434,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
'keywords' => $this->keywords ?: null,
'backlink_url' => $this->backlinkUrl ?: null,
'scheduled_at' => $this->publishMode === 'scheduled'
? $this->scheduledAtUtc()
? $this->resolveScheduledAt()
: null,
'embargo_at' => null,
]);

View file

@ -466,7 +466,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@if ($isBoosted)
<div class="rounded-[6px] border border-[color:var(--color-ok)]/30 bg-[color:var(--color-ok-soft)] px-3 py-2 text-[12.5px] text-[color:var(--color-gain-deep)]">
{{ __('Aktiv bis :date', ['date' => $boostedUntil?->copy()->setTimezone(\App\Models\PressRelease::DISPLAY_TIMEZONE)->format('d.m.Y H:i')]) }}
{{ __('Aktiv bis :date', ['date' => $boostedUntil?->format('d.m.Y H:i')]) }}
</div>
@endif