Neustrukturierung Customer / Lead / Booking Phase 2

This commit is contained in:
Kevin Adametz 2026-05-28 17:10:37 +02:00
parent 313f0dbf4e
commit 6df9c401af
69 changed files with 3809 additions and 374 deletions

View file

@ -11,38 +11,39 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Version eines Angebots (Modul 6).
* Eine konkrete Version (Snapshot) eines Angebots.
*
* Jede versendete Fassung wird hier festgehalten Texte, Positionen
* und PDF bleiben damit unveränderlich, sobald ein Kunde sie per
* Freigabe-Link einsehen kann. Neue Änderungen nach dem Versand
* erzeugen eine neue Version (version_no = max+1, status = draft).
* Alle inhaltlichen Felder (Texte, Preise, Gültigkeit, PDF-Pfad, Status,
* Anhänge) hängen hier nicht am Offer-Kopf. So bleiben bereits versendete
* Versionen unveränderlich (`isEditable() === false`), während eine neue
* Version ihre eigenen Texte/Positionen/Anhänge führt.
*
* @property int $id
* @property int $offer_id
* @property int $version_no
* @property string $status
* @property Carbon|null $valid_until
* @property float $total_price
* @property string|null $headline
* @property string|null $intro_text
* @property string|null $itinerary_text
* @property string|null $closing_text
* @property int|null $template_id
* @property string|null $pdf_path
* @property bool $pdf_archived
* @property Carbon|null $sent_at
* @property Carbon|null $accepted_at
* @property string|null $accepted_via
* @property array|null $template_document_ids
* @property int $created_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property-read Offer $offer
* @property-read OfferTemplate|null $template
* @property-read Collection|OfferItem[] $items
* @property-read Collection|OfferFile[] $files
* @property-read User $creator
* @property int $id
* @property int $offer_id
* @property int $version_no
* @property string $status draft|sent|accepted|declined|expired|superseded
* @property Carbon|null $valid_until
* @property float $total_price
* @property string|null $headline
* @property string|null $intro_text
* @property string|null $itinerary_text
* @property string|null $closing_text
* @property int|null $template_id
* @property string|null $pdf_path
* @property bool $pdf_archived
* @property Carbon|null $sent_at
* @property Carbon|null $accepted_at
* @property string|null $accepted_via customer_link|admin|email
* @property array|null $template_document_ids JSON-Array von zentral hinterlegten Dokument-IDs
* @property int $created_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property-read Offer $offer
* @property-read Collection|OfferItem[] $items
* @property-read Collection|OfferFile[] $files
* @property-read Collection|OfferAccessToken[] $tokens
* @property-read OfferTemplate|null $template
* @property-read User $createdBy
*/
class OfferVersion extends Model
{
@ -57,8 +58,9 @@ class OfferVersion extends Model
public const ACCEPTED_VIA_LINK = 'customer_link';
public const ACCEPTED_VIA_ADMIN = 'admin';
public const ACCEPTED_VIA_MAIL = 'email';
public const ACCEPTED_VIA_EMAIL = 'email';
protected $connection = 'mysql';
protected $table = 'offer_versions';
protected $fillable = [
@ -84,19 +86,36 @@ class OfferVersion extends Model
protected $casts = [
'offer_id' => 'int',
'version_no' => 'int',
'valid_until' => 'date',
'total_price' => 'decimal:2',
'template_id' => 'int',
'pdf_archived' => 'bool',
'created_by' => 'int',
'total_price' => 'decimal:2',
'valid_until' => 'date',
'sent_at' => 'datetime',
'accepted_at' => 'datetime',
'pdf_archived' => 'bool',
'template_document_ids' => 'array',
'created_by' => 'int',
];
// ── Beziehungen ──────────────────────────────────────────────────────────
public function offer(): BelongsTo
{
return $this->belongsTo(Offer::class);
return $this->belongsTo(Offer::class, 'offer_id');
}
public function items(): HasMany
{
return $this->hasMany(OfferItem::class, 'offer_version_id')->orderBy('position');
}
public function files(): HasMany
{
return $this->hasMany(OfferFile::class, 'offer_version_id');
}
public function tokens(): HasMany
{
return $this->hasMany(OfferAccessToken::class, 'offer_version_id');
}
public function template(): BelongsTo
@ -104,23 +123,52 @@ class OfferVersion extends Model
return $this->belongsTo(OfferTemplate::class, 'template_id');
}
public function items(): HasMany
{
return $this->hasMany(OfferItem::class)->orderBy('position');
}
public function files(): HasMany
{
return $this->hasMany(OfferFile::class);
}
public function creator(): BelongsTo
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
/**
* Darf diese Version noch bearbeitet werden?
* Nur Draft-Versionen sind editierbar; alle anderen sind eingefroren.
*/
public function isEditable(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* Liefert den zuletzt erzeugten, noch nicht widerrufenen Token
* (falls vorhanden) wird im Freigabe-Link verwendet.
*/
public function latestToken(): ?OfferAccessToken
{
return $this->tokens()
->whereNull('revoked_at')
->orderByDesc('id')
->first();
}
/**
* Formatierter Preis („1.234,56 "), konsistent mit der Darstellung
* im Buchungsdetail.
*/
public function totalPriceFormatted(): string
{
return number_format((float) $this->total_price, 2, ',', '.') . ' €';
}
/**
* Gibt die URL auf das zuletzt erzeugte PDF dieser Version zurück,
* oder null, wenn noch kein PDF existiert.
*/
public function getPdfUrl(bool $download = false): ?string
{
if (! $this->pdf_path) {
return null;
}
return route('offer_pdf', ['versionId' => $this->id, 'do' => $download ? 'download' : null]);
}
}