WS-8: doppelte Logik konsolidieren (Tags, Kontakt-Query, Billing)
- Themen-Tag-Verwaltung (addTag/removeTag/tagsArray) war in create- und edit-Editor byte-identisch dupliziert -> Trait App\Livewire\Concerns\InteractsWithPressReleaseTags. - Kontaktauswahl je Firma (withoutGlobalScopes + where + Sortierung) aus create/edit/dashboard in Contact::scopeForCompany() zentralisiert. - Rechnungsadress-Pflichtfelder als BillingAddress::REQUIRED_FIELDS; isComplete() und neues completenessPercent() teilen die Liste, Dashboard nutzt es statt eigener Feldliste. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1ff1cc3626
commit
1aa43914da
6 changed files with 115 additions and 134 deletions
68
app/Livewire/Concerns/InteractsWithPressReleaseTags.php
Normal file
68
app/Livewire/Concerns/InteractsWithPressReleaseTags.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Concerns;
|
||||
|
||||
/**
|
||||
* Themen-Tag-Verwaltung für die PM-Editoren (Erstellen/Bearbeiten).
|
||||
*
|
||||
* Tags werden als kommaseparierte Liste in der string-Property `$keywords`
|
||||
* gehalten (max. 5, eindeutig). Die nutzende Livewire-Komponente muss eine
|
||||
* `$keywords`-Property sowie die Computed-Properties `tags` und
|
||||
* `presubmitChecks` bereitstellen — diese werden nach jeder Änderung
|
||||
* invalidiert.
|
||||
*/
|
||||
trait InteractsWithPressReleaseTags
|
||||
{
|
||||
public function addTag(string $tag): void
|
||||
{
|
||||
$tag = trim($tag);
|
||||
|
||||
if ($tag === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $this->tagsArray();
|
||||
|
||||
if (count($existing) >= 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($tag, $existing, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing[] = $tag;
|
||||
$this->keywords = implode(', ', $existing);
|
||||
|
||||
unset($this->tags, $this->presubmitChecks);
|
||||
}
|
||||
|
||||
public function removeTag(string $tag): void
|
||||
{
|
||||
$existing = array_values(array_filter(
|
||||
$this->tagsArray(),
|
||||
fn (string $existingTag): bool => $existingTag !== $tag,
|
||||
));
|
||||
|
||||
$this->keywords = implode(', ', $existing);
|
||||
|
||||
unset($this->tags, $this->presubmitChecks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function tagsArray(): array
|
||||
{
|
||||
if (trim($this->keywords) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect(explode(',', $this->keywords))
|
||||
->map(fn (string $tag): string => trim($tag))
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
|
@ -34,13 +34,27 @@ class BillingAddress extends Model
|
|||
/**
|
||||
* Ist die Adresse vollständig genug für eine Rechnung bzw. Buchung?
|
||||
* Maßgeblich: Empfänger (Name), Straße, PLZ, Ort und Land.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public const REQUIRED_FIELDS = ['name', 'address1', 'postal_code', 'city', 'country_code'];
|
||||
|
||||
public function isComplete(): bool
|
||||
{
|
||||
return filled($this->name)
|
||||
&& filled($this->address1)
|
||||
&& filled($this->postal_code)
|
||||
&& filled($this->city)
|
||||
&& filled($this->country_code);
|
||||
return collect(self::REQUIRED_FIELDS)
|
||||
->every(fn (string $field): bool => filled($this->{$field}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Anteil der ausgefüllten Pflichtfelder in Prozent (0–100) — für die
|
||||
* Fortschritts-Anzeige im Dashboard.
|
||||
*/
|
||||
public function completenessPercent(): int
|
||||
{
|
||||
$filled = collect(self::REQUIRED_FIELDS)
|
||||
->filter(fn (string $field): bool => filled($this->{$field}))
|
||||
->count();
|
||||
|
||||
return (int) round(($filled / count(self::REQUIRED_FIELDS)) * 100);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Models;
|
|||
use App\Enums\Portal;
|
||||
use App\Scopes\PortalScope;
|
||||
use Database\Factories\ContactFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
|
@ -21,6 +22,19 @@ class Contact extends Model
|
|||
static::addGlobalScope(new PortalScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kontakte einer Firma, portal-unabhängig (ohne GlobalScope) und in der
|
||||
* fürs Kunden-Panel üblichen Sortierung. Single Source of Truth für die
|
||||
* Kontaktauswahl in Erstellen/Bearbeiten und für Kontakt-Zählungen.
|
||||
*/
|
||||
public function scopeForCompany(Builder $query, int $companyId): Builder
|
||||
{
|
||||
return $query->withoutGlobalScopes()
|
||||
->where('company_id', $companyId)
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name');
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'portal',
|
||||
|
|
|
|||
|
|
@ -117,14 +117,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
|||
*/
|
||||
private function billingCompleteness(?BillingAddress $address): int
|
||||
{
|
||||
if (! $address) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$fields = ['name', 'address1', 'postal_code', 'city', 'country_code'];
|
||||
$filled = collect($fields)->filter(fn (string $field) => filled($address->{$field}))->count();
|
||||
|
||||
return (int) round(($filled / count($fields)) * 100);
|
||||
return $address?->completenessPercent() ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -166,9 +159,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
|||
}
|
||||
|
||||
if ($selectedCompany) {
|
||||
$contactsCount = Contact::withoutGlobalScopes()
|
||||
->where('company_id', $selectedCompany->id)
|
||||
->count();
|
||||
$contactsCount = Contact::forCompany($selectedCompany->id)->count();
|
||||
|
||||
if ($contactsCount === 0) {
|
||||
$hints[] = [
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ use Livewire\Volt\Component;
|
|||
|
||||
new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class extends Component
|
||||
{
|
||||
use \App\Livewire\Concerns\InteractsWithPressReleaseTags;
|
||||
|
||||
public string $portal = 'presseecho';
|
||||
|
||||
public string $language = 'de';
|
||||
|
|
@ -299,41 +301,8 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
}
|
||||
}
|
||||
|
||||
public function addTag(string $tag): void
|
||||
{
|
||||
$tag = trim($tag);
|
||||
|
||||
if ($tag === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $this->tagsArray();
|
||||
|
||||
if (count($existing) >= 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($tag, $existing, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing[] = $tag;
|
||||
$this->keywords = implode(', ', $existing);
|
||||
|
||||
unset($this->tags, $this->presubmitChecks);
|
||||
}
|
||||
|
||||
public function removeTag(string $tag): void
|
||||
{
|
||||
$existing = array_values(array_filter(
|
||||
$this->tagsArray(),
|
||||
fn (string $existingTag): bool => $existingTag !== $tag,
|
||||
));
|
||||
|
||||
$this->keywords = implode(', ', $existing);
|
||||
|
||||
unset($this->tags, $this->presubmitChecks);
|
||||
}
|
||||
// Tag-Verwaltung (addTag/removeTag/tagsArray) liegt im Trait
|
||||
// App\Livewire\Concerns\InteractsWithPressReleaseTags.
|
||||
|
||||
/**
|
||||
* Lazy Auto-Draft: legt sofort einen Entwurf an, damit Titelbild und
|
||||
|
|
@ -662,22 +631,6 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function tagsArray(): array
|
||||
{
|
||||
if (trim($this->keywords) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect(explode(',', $this->keywords))
|
||||
->map(fn (string $tag): string => trim($tag))
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function defaultContactIdFor(int $companyId): ?int
|
||||
{
|
||||
|
|
@ -689,19 +642,13 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
*/
|
||||
private function companyContacts(int $companyId): Collection
|
||||
{
|
||||
return Contact::withoutGlobalScopes()
|
||||
->where('company_id', $companyId)
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
return Contact::forCompany($companyId)
|
||||
->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']);
|
||||
}
|
||||
|
||||
private function companyContact(int $contactId, int $companyId): ?Contact
|
||||
{
|
||||
return Contact::withoutGlobalScopes()
|
||||
->where('company_id', $companyId)
|
||||
->whereKey($contactId)
|
||||
->first();
|
||||
return Contact::forCompany($companyId)->whereKey($contactId)->first();
|
||||
}
|
||||
|
||||
private function selectedCompany(): ?Company
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ use Livewire\Volt\Component;
|
|||
|
||||
new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] class extends Component
|
||||
{
|
||||
use \App\Livewire\Concerns\InteractsWithPressReleaseTags;
|
||||
|
||||
#[Locked]
|
||||
public int $id;
|
||||
|
||||
|
|
@ -182,41 +184,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
$this->validateScheduledAtWhenReady();
|
||||
}
|
||||
|
||||
public function addTag(string $tag): void
|
||||
{
|
||||
$tag = trim($tag);
|
||||
|
||||
if ($tag === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $this->tagsArray();
|
||||
|
||||
if (count($existing) >= 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($tag, $existing, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing[] = $tag;
|
||||
$this->keywords = implode(', ', $existing);
|
||||
|
||||
unset($this->tags, $this->presubmitChecks);
|
||||
}
|
||||
|
||||
public function removeTag(string $tag): void
|
||||
{
|
||||
$existing = array_values(array_filter(
|
||||
$this->tagsArray(),
|
||||
fn (string $existingTag): bool => $existingTag !== $tag,
|
||||
));
|
||||
|
||||
$this->keywords = implode(', ', $existing);
|
||||
|
||||
unset($this->tags, $this->presubmitChecks);
|
||||
}
|
||||
// Tag-Verwaltung (addTag/removeTag/tagsArray) liegt im Trait
|
||||
// App\Livewire\Concerns\InteractsWithPressReleaseTags.
|
||||
|
||||
/**
|
||||
* Live-Re-Validation: sobald für ein Property bereits ein Error im Bag
|
||||
|
|
@ -638,22 +607,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function tagsArray(): array
|
||||
{
|
||||
if (trim($this->keywords) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect(explode(',', $this->keywords))
|
||||
->map(fn (string $tag): string => trim($tag))
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function defaultContactIdFor(int $companyId): ?int
|
||||
{
|
||||
|
|
@ -665,19 +618,13 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
*/
|
||||
private function companyContacts(int $companyId): Collection
|
||||
{
|
||||
return Contact::withoutGlobalScopes()
|
||||
->where('company_id', $companyId)
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
return Contact::forCompany($companyId)
|
||||
->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']);
|
||||
}
|
||||
|
||||
private function companyContact(int $contactId, int $companyId): ?Contact
|
||||
{
|
||||
return Contact::withoutGlobalScopes()
|
||||
->where('company_id', $companyId)
|
||||
->whereKey($contactId)
|
||||
->first();
|
||||
return Contact::forCompany($companyId)->whereKey($contactId)->first();
|
||||
}
|
||||
|
||||
private function getMyPR(): PressRelease
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue