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?
|
* Ist die Adresse vollständig genug für eine Rechnung bzw. Buchung?
|
||||||
* Maßgeblich: Empfänger (Name), Straße, PLZ, Ort und Land.
|
* 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
|
public function isComplete(): bool
|
||||||
{
|
{
|
||||||
return filled($this->name)
|
return collect(self::REQUIRED_FIELDS)
|
||||||
&& filled($this->address1)
|
->every(fn (string $field): bool => filled($this->{$field}));
|
||||||
&& filled($this->postal_code)
|
}
|
||||||
&& filled($this->city)
|
|
||||||
&& filled($this->country_code);
|
/**
|
||||||
|
* 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\Enums\Portal;
|
||||||
use App\Scopes\PortalScope;
|
use App\Scopes\PortalScope;
|
||||||
use Database\Factories\ContactFactory;
|
use Database\Factories\ContactFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
@ -21,6 +22,19 @@ class Contact extends Model
|
||||||
static::addGlobalScope(new PortalScope);
|
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 = [
|
protected $fillable = [
|
||||||
'company_id',
|
'company_id',
|
||||||
'portal',
|
'portal',
|
||||||
|
|
|
||||||
|
|
@ -117,14 +117,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
||||||
*/
|
*/
|
||||||
private function billingCompleteness(?BillingAddress $address): int
|
private function billingCompleteness(?BillingAddress $address): int
|
||||||
{
|
{
|
||||||
if (! $address) {
|
return $address?->completenessPercent() ?? 0;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -166,9 +159,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($selectedCompany) {
|
if ($selectedCompany) {
|
||||||
$contactsCount = Contact::withoutGlobalScopes()
|
$contactsCount = Contact::forCompany($selectedCompany->id)->count();
|
||||||
->where('company_id', $selectedCompany->id)
|
|
||||||
->count();
|
|
||||||
|
|
||||||
if ($contactsCount === 0) {
|
if ($contactsCount === 0) {
|
||||||
$hints[] = [
|
$hints[] = [
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ use Livewire\Volt\Component;
|
||||||
|
|
||||||
new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class extends Component
|
new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class extends Component
|
||||||
{
|
{
|
||||||
|
use \App\Livewire\Concerns\InteractsWithPressReleaseTags;
|
||||||
|
|
||||||
public string $portal = 'presseecho';
|
public string $portal = 'presseecho';
|
||||||
|
|
||||||
public string $language = 'de';
|
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-Verwaltung (addTag/removeTag/tagsArray) liegt im Trait
|
||||||
{
|
// App\Livewire\Concerns\InteractsWithPressReleaseTags.
|
||||||
$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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy Auto-Draft: legt sofort einen Entwurf an, damit Titelbild und
|
* 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
|
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
|
private function companyContacts(int $companyId): Collection
|
||||||
{
|
{
|
||||||
return Contact::withoutGlobalScopes()
|
return Contact::forCompany($companyId)
|
||||||
->where('company_id', $companyId)
|
|
||||||
->orderBy('last_name')
|
|
||||||
->orderBy('first_name')
|
|
||||||
->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']);
|
->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function companyContact(int $contactId, int $companyId): ?Contact
|
private function companyContact(int $contactId, int $companyId): ?Contact
|
||||||
{
|
{
|
||||||
return Contact::withoutGlobalScopes()
|
return Contact::forCompany($companyId)->whereKey($contactId)->first();
|
||||||
->where('company_id', $companyId)
|
|
||||||
->whereKey($contactId)
|
|
||||||
->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function selectedCompany(): ?Company
|
private function selectedCompany(): ?Company
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ use Livewire\Volt\Component;
|
||||||
|
|
||||||
new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] class extends Component
|
new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] class extends Component
|
||||||
{
|
{
|
||||||
|
use \App\Livewire\Concerns\InteractsWithPressReleaseTags;
|
||||||
|
|
||||||
#[Locked]
|
#[Locked]
|
||||||
public int $id;
|
public int $id;
|
||||||
|
|
||||||
|
|
@ -182,41 +184,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
||||||
$this->validateScheduledAtWhenReady();
|
$this->validateScheduledAtWhenReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addTag(string $tag): void
|
// Tag-Verwaltung (addTag/removeTag/tagsArray) liegt im Trait
|
||||||
{
|
// App\Livewire\Concerns\InteractsWithPressReleaseTags.
|
||||||
$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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live-Re-Validation: sobald für ein Property bereits ein Error im Bag
|
* 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
|
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
|
private function companyContacts(int $companyId): Collection
|
||||||
{
|
{
|
||||||
return Contact::withoutGlobalScopes()
|
return Contact::forCompany($companyId)
|
||||||
->where('company_id', $companyId)
|
|
||||||
->orderBy('last_name')
|
|
||||||
->orderBy('first_name')
|
|
||||||
->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']);
|
->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function companyContact(int $contactId, int $companyId): ?Contact
|
private function companyContact(int $contactId, int $companyId): ?Contact
|
||||||
{
|
{
|
||||||
return Contact::withoutGlobalScopes()
|
return Contact::forCompany($companyId)->whereKey($contactId)->first();
|
||||||
->where('company_id', $companyId)
|
|
||||||
->whereKey($contactId)
|
|
||||||
->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getMyPR(): PressRelease
|
private function getMyPR(): PressRelease
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue