'int', 'version_no' => 'int', 'template_id' => 'int', 'created_by' => 'int', 'total_price' => 'decimal:2', 'valid_until' => 'date', 'sent_at' => 'datetime', 'accepted_at' => 'datetime', 'pdf_archived' => 'bool', 'template_document_ids' => 'array', ]; // ── Beziehungen ────────────────────────────────────────────────────────── public function offer(): BelongsTo { 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 { return $this->belongsTo(OfferTemplate::class, 'template_id'); } 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]); } }