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

@ -0,0 +1,104 @@
<?php
namespace App\Http\Requests\Offer;
use App\Models\OfferItem;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class OfferTemplateRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'branch_id' => 'nullable|integer|exists:branch,id',
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:5000',
'default_headline' => 'nullable|string|max:500',
'default_intro' => 'nullable|string|max:65000',
'default_itinerary' => 'nullable|string|max:65000',
'default_closing' => 'nullable|string|max:65000',
'default_items' => 'nullable|array|max:200',
'default_items.*' => 'array',
'default_items.*.type' => ['required', Rule::in(OfferItem::TYPES)],
'default_items.*.title' => 'required|string|max:1000',
'default_items.*.description' => 'nullable|string|max:20000',
'default_items.*.quantity' => 'required|integer|min:1',
'default_items.*.price_per_unit' => 'required|numeric',
'default_items.*.travel_program_id' => 'nullable|integer|min:0',
'default_items.*.fewo_lodging_id' => 'nullable|integer|min:0',
'default_items.*.metadata' => 'nullable|array',
'is_active' => 'nullable|boolean',
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $v) {
if ($v->fails()) {
return;
}
$rows = $this->input('default_items', []) ?? [];
foreach (array_values($rows) as $i => $row) {
if (! is_array($row)) {
continue;
}
$type = $row['type'] ?? null;
$p = $row['price_per_unit'] ?? null;
if ($type === null || $p === null) {
continue;
}
if (in_array($type, [OfferItem::TYPE_TRAVEL, OfferItem::TYPE_SERVICE, OfferItem::TYPE_OPTION, OfferItem::TYPE_INSURANCE, OfferItem::TYPE_CUSTOM], true)) {
if ((float) $p < 0) {
$v->errors()->add("default_items.$i.price_per_unit", 'Der Einzelpreis muss mindestens 0 sein (außer bei Rabatten).');
}
}
}
});
}
protected function prepareForValidation(): void
{
if ($this->has('default_headline') && is_string($this->input('default_headline'))) {
$this->merge(['default_headline' => strip_tags($this->input('default_headline'))]);
}
$this->merge(
$this->sanitizeWysiwygInput([
'default_intro' => $this->input('default_intro'),
'default_itinerary' => $this->input('default_itinerary'),
'default_closing' => $this->input('default_closing'),
])
);
}
/**
* @param array<string, mixed> $fields
* @return array<string, string|null>
*/
protected function sanitizeWysiwygInput(array $fields): array
{
$allowed = '<p><br><br/><strong><b><em><i><u><ul><ol><li><a><h1><h2><h3><h4><h5><h6><blockquote><span><div><table><thead><tbody><tr><th><td>';
$out = [];
foreach ($fields as $key => $val) {
if ($val === null) {
$out[$key] = $val;
continue;
}
if (! is_string($val)) {
$out[$key] = (string) $val;
continue;
}
$out[$key] = strip_tags($val, $allowed);
}
return $out;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Offer;
use Illuminate\Foundation\Http\FormRequest;
class SendOfferRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'subject' => 'required|string|max:500',
'body' => 'required|string|max:100000',
'cc' => 'nullable|string|max:2000',
'bcc' => 'nullable|string|max:2000',
'reply_to' => 'nullable|email|max:255',
'expires_at' => 'nullable|date|after:now',
];
}
public function messages(): array
{
return [
'subject.required' => 'Bitte einen Betreff angeben.',
'body.required' => 'Bitte den E-Mail-Text angeben.',
'expires_at.after' => 'Ablaufdatum muss in der Zukunft liegen.',
];
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests\Offer;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class StoreOfferRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'contact_id' => [
'nullable',
'integer',
Rule::exists('contacts', 'id'),
],
'inquiry_id' => [
'nullable',
'integer',
Rule::exists('inquiries', 'id'),
],
'template_id' => [
'nullable',
'integer',
Rule::exists('offer_templates', 'id')->whereNull('deleted_at'),
],
'booking_id' => [
'nullable',
'integer',
Rule::exists('booking', 'id'),
],
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $v) {
if (! $v->fails() && $this->input('contact_id') === null && $this->input('inquiry_id') === null) {
$v->errors()->add('contact_id', 'Bitte wähle einen Kontakt oder eine Anfrage.');
}
});
}
public function messages(): array
{
return [
'contact_id.exists' => 'Der gewählte Kontakt existiert nicht.',
'inquiry_id.exists' => 'Die gewählte Anfrage existiert nicht.',
'template_id.exists' => 'Die gewählte Vorlage existiert (nicht) oder ist gelöscht.',
'booking_id.exists' => 'Die gewählte Buchung existiert nicht.',
];
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace App\Http\Requests\Offer;
use App\Models\OfferItem;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class UpdateVersionRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
public function rules(): array
{
return [
'headline' => 'nullable|string|max:500',
'intro_text' => 'nullable|string|max:65000',
'itinerary_text' => 'nullable|string|max:65000',
'closing_text' => 'nullable|string|max:65000',
'valid_until' => 'nullable|date|after_or_equal:today',
'template_id' => [
'nullable',
'integer',
Rule::exists('offer_templates', 'id')->whereNull('deleted_at'),
],
'template_document_ids' => 'nullable|array',
'template_document_ids.*' => 'integer',
'items' => 'nullable|array|max:200',
'items.*' => 'array',
'items.*.id' => 'nullable|integer|exists:offer_items,id',
'items.*.type' => ['required', Rule::in(OfferItem::TYPES)],
'items.*.title' => 'required|string|max:1000',
'items.*.description' => 'nullable|string|max:20000',
'items.*.quantity' => 'required|integer|min:1',
'items.*.price_per_unit' => 'required|numeric',
'items.*.travel_program_id' => 'nullable|integer|min:0',
'items.*.fewo_lodging_id' => 'nullable|integer|min:0',
'items.*.metadata' => 'nullable|array',
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $v) {
if ($v->fails()) {
return;
}
$items = $this->input('items', []);
foreach (array_values($items) as $i => $row) {
if (! is_array($row)) {
continue;
}
$type = $row['type'] ?? null;
$p = $row['price_per_unit'] ?? null;
if ($type === null || $p === null) {
continue;
}
if (in_array($type, [OfferItem::TYPE_TRAVEL, OfferItem::TYPE_SERVICE, OfferItem::TYPE_OPTION, OfferItem::TYPE_INSURANCE, OfferItem::TYPE_CUSTOM], true)) {
if ((float) $p < 0) {
$v->errors()->add("items.$i.price_per_unit", 'Der Einzelpreis muss mindestens 0 sein (außer bei Rabatten).');
}
}
}
});
}
protected function prepareForValidation(): void
{
if ($this->has('headline') && is_string($this->input('headline'))) {
$this->merge(['headline' => strip_tags($this->input('headline'))]);
}
$this->merge($this->sanitizeWysiwygFields([
'intro_text' => $this->input('intro_text'),
'itinerary_text' => $this->input('itinerary_text'),
'closing_text' => $this->input('closing_text'),
]));
}
/**
* Einfache WYSIWYG-Whitelist (TinyMCE/TipTap): gefährliche Tags raus, Basis-Formatierung behalten.
*
* @param array<string, mixed> $fields
* @return array<string, string>
*/
protected function sanitizeWysiwygFields(array $fields): array
{
$allowed = '<p><br><br/><strong><b><em><i><u><ul><ol><li><a><h1><h2><h3><h4><h5><h6><blockquote><span><div><table><thead><tbody><tr><th><td>';
$out = [];
foreach ($fields as $key => $val) {
if ($val === null) {
$out[$key] = $val;
continue;
}
if (! is_string($val)) {
$out[$key] = (string) $val;
continue;
}
$out[$key] = strip_tags($val, $allowed);
}
return $out;
}
}