174 lines
5.6 KiB
PHP
174 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
|
/**
|
|
* Eine konkrete Version (Snapshot) eines Angebots.
|
|
*
|
|
* 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 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
|
|
{
|
|
use HasFactory;
|
|
|
|
public const STATUS_DRAFT = 'draft';
|
|
public const STATUS_SENT = 'sent';
|
|
public const STATUS_ACCEPTED = 'accepted';
|
|
public const STATUS_DECLINED = 'declined';
|
|
public const STATUS_EXPIRED = 'expired';
|
|
public const STATUS_SUPERSEDED = 'superseded';
|
|
|
|
public const ACCEPTED_VIA_LINK = 'customer_link';
|
|
public const ACCEPTED_VIA_ADMIN = 'admin';
|
|
public const ACCEPTED_VIA_EMAIL = 'email';
|
|
|
|
protected $connection = 'mysql';
|
|
protected $table = 'offer_versions';
|
|
|
|
protected $fillable = [
|
|
'offer_id',
|
|
'version_no',
|
|
'status',
|
|
'valid_until',
|
|
'total_price',
|
|
'headline',
|
|
'intro_text',
|
|
'itinerary_text',
|
|
'closing_text',
|
|
'template_id',
|
|
'pdf_path',
|
|
'pdf_archived',
|
|
'sent_at',
|
|
'accepted_at',
|
|
'accepted_via',
|
|
'template_document_ids',
|
|
'created_by',
|
|
];
|
|
|
|
protected $casts = [
|
|
'offer_id' => '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]);
|
|
}
|
|
}
|