Compare commits

..

28 Commits

Author SHA1 Message Date
philipp lang a456cc1889 Fix: Sort participants by created_at
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-12-16 14:18:02 +01:00
philipp lang 784b49c3dd Add invoice full text search
continuous-integration/drone/push Build is failing Details
2024-12-15 16:05:38 +01:00
philipp lang c3a0381a0a Fix invoice settings when fields are empty
continuous-integration/drone/push Build is passing Details
2024-12-15 12:06:44 +01:00
philipp lang 101ac7402a Add Phpunit to gitignore
continuous-integration/drone/push Build is passing Details
2024-12-15 11:56:13 +01:00
philipp lang 2c38488962 Remove cross
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-12-13 19:13:30 +01:00
philipp lang 8a26276e28 Rename document
continuous-integration/drone/push Build is passing Details
2024-12-13 19:03:40 +01:00
philipp lang 4651a4cb96 Lint
continuous-integration/drone/push Build is passing Details
2024-12-13 14:37:13 +01:00
philipp lang 688e10e230 Lint
continuous-integration/drone/push Build is failing Details
2024-12-13 11:56:02 +01:00
philipp lang 267da38c6b Lint
continuous-integration/drone/push Build is passing Details
2024-12-13 02:46:44 +01:00
philipp lang 499c61afff Lint 2024-12-13 02:44:08 +01:00
philipp lang 02531938b3 Lint 2024-12-13 02:42:08 +01:00
philipp lang 1c53e81f1a Lint 2024-12-13 02:40:33 +01:00
philipp lang 98712093b2 Lint 2024-12-13 02:36:17 +01:00
philipp lang a32ef33113 Move engine 2024-12-13 02:33:12 +01:00
philipp lang 088ea4f0a2 Lint 2024-12-13 02:28:41 +01:00
philipp lang 4ac89da124 Lint 2024-12-13 02:16:09 +01:00
philipp lang 286ceea0d4 Lint 2024-12-13 01:59:24 +01:00
philipp lang 52ac5937a4 Add validation 2024-12-13 01:59:09 +01:00
philipp lang 4690e86bbf Add gallier document 2024-12-13 01:54:02 +01:00
philipp lang 1a4a7c3652 Add base gallier document 2024-12-13 00:58:44 +01:00
philipp lang b05c5025c0 Add button to mark invoice as paid
continuous-integration/drone/push Build is passing Details
2024-12-12 21:26:51 +01:00
philipp lang acf7b94094 Update CHANGELOG
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is passing Details
2024-12-12 16:37:20 +01:00
philipp lang 57b1efb065 Add registration_from and registration_until to frontend api
continuous-integration/drone/push Build is passing Details
2024-12-12 15:42:35 +01:00
philipp lang 3e563f8390 Add registration_from and registration_to fields 2024-12-12 15:21:03 +01:00
philipp lang 0a5590d533 Remove CreatesApplication Trait 2024-12-12 13:56:13 +01:00
philipp lang 10deaf15a1 Update form edit view 2024-12-12 13:45:42 +01:00
philipp lang ae447a7a60 Add fileshare and web setting on bin/run 2024-12-12 13:45:26 +01:00
philipp lang baea21807a Update CHANGELOG
continuous-integration/drone/push Build is passing Details
2024-12-12 13:21:05 +01:00
43 changed files with 672 additions and 504 deletions

1
.gitignore vendored
View File

@ -40,3 +40,4 @@ Homestead.json
/public/sprite.svg
/.php-cs-fixer.cache
/groups.sql
/.phpunit.cache

View File

@ -1,5 +1,14 @@
# Letzte Änderungen
### 1.12.2
- Zuschussliste Gallier
### 1.12.1
- In Teilnehmer-Liste von Veranstaltungen kann nun sortiert und gefiltert werden.
- Formulare: Feld Registrierung von / bis
### 1.11.5
- Fix: Synchronisation von NaMi-Mitgliedern

View File

@ -8,6 +8,7 @@ use App\Contribution\Documents\RdpNrwDocument;
use App\Contribution\Documents\CityRemscheidDocument;
use App\Contribution\Documents\CitySolingenDocument;
use App\Contribution\Documents\CityFrankfurtMainDocument;
use App\Contribution\Documents\WuppertalDocument;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
@ -22,6 +23,7 @@ class ContributionFactory
CityRemscheidDocument::class,
CityFrankfurtMainDocument::class,
BdkjHesse::class,
WuppertalDocument::class,
];
/**
@ -30,7 +32,7 @@ class ContributionFactory
public function compilerSelect(): Collection
{
return collect($this->documents)->map(fn ($document) => [
'title' => $document::getName(),
'title' => $document::buttonName(),
'class' => $document,
]);
}

View File

@ -72,13 +72,23 @@ class MemberData extends Data
return $this->zip . ' ' . $this->location;
}
public function age(): string
public function age(): int
{
return (string) $this->birthday->diffInYears(now()) ?: '';
return intval($this->birthday->diffInYears(now()));
}
public function birthYear(): string
{
return (string) $this->birthday->year;
}
public function birthdayHuman(): string
{
return $this->birthday->format('d.m.Y');
}
public function genderLetter(): string
{
return $this->gender->short;
}
}

View File

@ -3,15 +3,16 @@
namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class BdkjHesse extends ContributionDocument
{
use HasPdfBackground;
/**
* @param Collection<int, Collection<int, MemberData>> $members
*/
@ -25,6 +26,7 @@ class BdkjHesse extends ContributionDocument
public ?string $filename = '',
public string $type = 'F',
) {
$this->setEventName($eventName);
}
public function dateFrom(): string
@ -114,36 +116,9 @@ class BdkjHesse extends ContributionDocument
return $member->birthYear();
}
public function basename(): string
{
return 'zuschuesse-bdkj-hessen' . Str::slug($this->eventName);
}
public function view(): string
{
return 'tex.contribution.bdkj-hesse';
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function setFilename(string $filename): static
{
$this->filename = $filename;
return $this;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string
{
return 'Für BDKJ Hessen erstellen';
return 'BDKJ Hessen';
}
/**
@ -156,7 +131,6 @@ class BdkjHesse extends ContributionDocument
'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string',
'eventName' => 'required|string',
];
}
}

View File

@ -3,16 +3,17 @@
namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use App\Invoice\InvoiceSettings;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class CityFrankfurtMainDocument extends ContributionDocument
{
use HasPdfBackground;
use FormatsDates;
public string $fromName;
/**
@ -28,6 +29,7 @@ class CityFrankfurtMainDocument extends ContributionDocument
public ?string $filename = '',
public string $type = 'F',
) {
$this->setEventName($eventName);
$this->fromName = app(InvoiceSettings::class)->from_long;
}
@ -61,17 +63,6 @@ class CityFrankfurtMainDocument extends ContributionDocument
);
}
public function dateFromHuman(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function dateUntilHuman(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public function countryName(): string
{
return $this->country->name;
@ -82,56 +73,9 @@ class CityFrankfurtMainDocument extends ContributionDocument
return count($this->members);
}
public function memberShort(MemberData $member): string
{
return $member->isLeader ? 'L' : '';
}
public function memberName(MemberData $member): string
{
return $member->separatedName();
}
public function memberAddress(MemberData $member): string
{
return $member->fullAddress();
}
public function memberAge(MemberData $member): string
{
return $member->age();
}
public function basename(): string
{
return 'zuschuesse-frankfurt-' . Str::slug($this->eventName);
}
public function view(): string
{
return 'tex.contribution.city-frankfurt-main';
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function setFilename(string $filename): static
{
$this->filename = $filename;
return $this;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string
{
return 'Für Frankfurt erstellen';
return 'Frankfurt';
}
/**
@ -144,7 +88,6 @@ class CityFrankfurtMainDocument extends ContributionDocument
'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string',
'eventName' => 'required|string',
];
}
}

View File

@ -3,15 +3,17 @@
namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use App\Member\Member;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class CityRemscheidDocument extends ContributionDocument
{
use HasPdfBackground;
use FormatsDates;
/**
* @param Collection<int, Collection<int, Member>> $leaders
* @param Collection<int, Collection<int, Member>> $children
@ -25,17 +27,9 @@ class CityRemscheidDocument extends ContributionDocument
public Collection $children,
public ?string $filename = '',
public string $type = 'F',
public string $eventName = '',
) {
}
public function niceDateFrom(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function niceDateUntil(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
$this->setEventName($eventName);
}
/**
@ -52,6 +46,7 @@ class CityRemscheidDocument extends ContributionDocument
country: Country::where('id', $request['country'])->firstOrFail(),
leaders: $leaders->values()->toBase()->chunk(6),
children: $children->values()->toBase()->chunk(20),
eventName: $request['eventName'],
);
}
@ -70,39 +65,13 @@ class CityRemscheidDocument extends ContributionDocument
country: Country::where('id', $request['country'])->firstOrFail(),
leaders: $leaders->values()->toBase()->chunk(6),
children: $children->values()->toBase()->chunk(20),
eventName: $request['eventName'],
);
}
public function basename(): string
{
return 'zuschuesse-remscheid';
}
public function view(): string
{
return 'tex.contribution.city-remscheid';
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function setFilename(string $filename): static
{
$this->filename = $filename;
return $this;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string
{
return 'Für Remscheid erstellen';
return 'Remscheid';
}
/**

View File

@ -6,9 +6,7 @@ use App\Contribution\Data\MemberData;
use App\Invoice\InvoiceSettings;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class CitySolingenDocument extends ContributionDocument
{
@ -25,6 +23,7 @@ class CitySolingenDocument extends ContributionDocument
public string $eventName,
public string $type = 'F',
) {
$this->setEventName($eventName);
$this->fromName = app(InvoiceSettings::class)->from_long;
}
@ -74,11 +73,6 @@ class CitySolingenDocument extends ContributionDocument
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function checkboxes(): string
{
$output = '';
@ -94,16 +88,6 @@ class CitySolingenDocument extends ContributionDocument
return $firstRow . "\n" . $secondRow;
}
public function basename(): string
{
return 'zuschuesse-solingen-' . Str::slug($this->eventName);
}
public function view(): string
{
return 'tex.contribution.city-solingen';
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
@ -111,7 +95,7 @@ class CitySolingenDocument extends ContributionDocument
public static function getName(): string
{
return 'Für Stadt Solingen erstellen';
return 'Stadt Solingen';
}
/**
@ -123,7 +107,6 @@ class CitySolingenDocument extends ContributionDocument
'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d',
'zipLocation' => 'required|string',
'eventName' => 'required|string',
];
}
}

View File

@ -3,9 +3,12 @@
namespace App\Contribution\Documents;
use Zoomyboy\Tex\Document;
use Zoomyboy\Tex\Template;
abstract class ContributionDocument extends Document
{
private string $eventName;
abstract public static function getName(): string;
/**
@ -29,8 +32,34 @@ abstract class ContributionDocument extends Document
public static function globalRules(): array
{
return [
'eventName' => 'required|string',
'members' => 'present|array|min:1',
'members.*' => 'integer|exists:members,id',
];
}
public static function buttonName(): string
{
return 'Für ' . static::getName() . ' erstellen';;
}
public function setEventName(string $eventName): void
{
$this->eventName = $eventName;
}
public function basename(): string
{
return str('Zuschüsse ')->append($this->getName())->append(' ')->append($this->eventName)->slug();
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function view(): string
{
return 'tex.contribution.' . str(class_basename(static::class))->replace('Document', '')->kebab()->toString();
}
}

View File

@ -3,14 +3,16 @@
namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class RdpNrwDocument extends ContributionDocument
{
use HasPdfBackground;
use FormatsDates;
/**
* @param Collection<int, Collection<int, MemberData>> $members
*/
@ -22,14 +24,9 @@ class RdpNrwDocument extends ContributionDocument
public Collection $members,
public ?string $filename = '',
public string $type = 'F',
public string $eventName = '',
) {
}
public function dateRange(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y')
. ' - '
. Carbon::parse($this->dateUntil)->format('d.m.Y');
$this->setEventName($eventName);
}
/**
@ -43,6 +40,7 @@ class RdpNrwDocument extends ContributionDocument
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromModels($request['members'])->chunk(17),
eventName: $request['eventName'],
);
}
@ -57,6 +55,7 @@ class RdpNrwDocument extends ContributionDocument
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromApi($request['member_data'])->chunk(17),
eventName: $request['eventName'],
);
}
@ -65,65 +64,9 @@ class RdpNrwDocument extends ContributionDocument
return $this->country->name;
}
public function memberShort(MemberData $member): string
{
return $member->isLeader ? 'L' : '';
}
public function memberName(MemberData $member): string
{
return $member->separatedName();
}
public function memberAddress(MemberData $member): string
{
return $member->fullAddress();
}
public function memberGender(MemberData $member): string
{
if (!$member->gender) {
return '';
}
return strtolower(substr($member->gender->name, 0, 1));
}
public function memberAge(MemberData $member): string
{
return $member->age();
}
public function basename(): string
{
return 'zuschuesse-rdp-nrw';
}
public function view(): string
{
return 'tex.contribution.rdp-nrw';
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function setFilename(string $filename): static
{
$this->filename = $filename;
return $this;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string
{
return 'Für RdP NRW erstellen';
return 'RdP NRW';
}
/**
@ -136,7 +79,6 @@ class RdpNrwDocument extends ContributionDocument
'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string',
'eventName' => 'required|string',
];
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use Illuminate\Support\Collection;
class WuppertalDocument extends ContributionDocument
{
use HasPdfBackground;
use FormatsDates;
/**
* @param Collection<int, Collection<int, MemberData>> $members
*/
public function __construct(
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
public Collection $members,
public ?string $filename = '',
public string $type = 'F',
public string $eventName = '',
) {
$this->setEventName($eventName);
}
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): self
{
return new self(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromModels($request['members'])->chunk(14),
eventName: $request['eventName'],
);
}
/**
* {@inheritdoc}
*/
public static function fromApiRequest(array $request): self
{
return new self(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromApi($request['member_data'])->chunk(14),
eventName: $request['eventName'],
);
}
public static function getName(): string
{
return 'Wuppertal';
}
/**
* @return array<string, mixed>
*/
public static function rules(): array
{
return [
'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d',
'zipLocation' => 'required|string',
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Contribution\Traits;
use Carbon\Carbon;
trait FormatsDates
{
public function niceDateFrom(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function niceDateUntil(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public function dateRange(): string
{
return implode(' - ', [$this->niceDateFrom(), $this->niceDateUntil()]);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Contribution\Traits;
use Zoomyboy\Tex\Engine;
trait HasPdfBackground
{
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
}

View File

@ -7,6 +7,7 @@ use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Member\Member;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
@ -19,6 +20,10 @@ class RegisterAction
*/
public function handle(Form $form, array $input): Participant
{
if (!$form->canRegister()) {
throw ValidationException::withMessages(['event' => 'Anmeldung zzt nicht möglich.']);
}
$memberQuery = FieldCollection::fromRequest($form, $input)
->withNamiType()
->reduce(fn ($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery());

View File

@ -20,7 +20,7 @@ class UpdateParticipantSearchIndexAction
[
'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id'],
'searchableAttributes' => $form->getFields()->searchables()->getKeys(),
'sortableAttributes' => [...$form->getFields()->sortables()->getKeys(), 'id'],
'sortableAttributes' => [...$form->getFields()->sortables()->getKeys(), 'id', 'created_at'],
'displayedAttributes' => [...$form->getFields()->filterables()->getKeys(), ...$form->getFields()->searchables()->getKeys(), 'id'],
'pagination' => [
'maxTotalHits' => 1000000,

View File

@ -188,4 +188,17 @@ class Form extends Model implements HasMedia
{
return Sorting::from($this->meta['sorting']);
}
public function canRegister(): bool
{
if ($this->registration_from && $this->registration_from->gt(now())) {
return false;
}
if ($this->registration_until && $this->registration_until->lt(now())) {
return false;
}
return true;
}
}

View File

@ -112,6 +112,6 @@ class Participant extends Model implements Preventable
/** @return array<string, mixed> */
public function toSearchableArray(): array
{
return [...$this->data, 'parent-id' => $this->parent_id];
return [...$this->data, 'parent-id' => $this->parent_id, 'created_at' => $this->created_at->timestamp];
}
}

View File

@ -35,6 +35,7 @@ class FormApiResource extends JsonResource
'image' => $this->getMedia('headerImage')->first()->getFullUrl('square'),
'is_active' => $this->is_active,
'is_private' => $this->is_private,
'can_register' => $this->getModel()->canRegister(),
];
}

View File

@ -25,6 +25,15 @@ class Gender extends Model
};
}
public function getShortAttribute(): string
{
return match ($this->name) {
'Männlich' => 'm',
'Weiblich' => 'w',
default => ''
};
}
public static function fromString(string $title): self
{
return self::firstWhere('name', $title);

View File

@ -6,9 +6,9 @@ use Lorisleiva\Actions\Concerns\AsAction;
use App\Invoice\Models\Invoice;
use App\Invoice\Resources\InvoiceResource;
use App\Invoice\Scopes\InvoiceFilterScope;
use Illuminate\Pagination\LengthAwarePaginator;
use Inertia\Inertia;
use Inertia\Response;
use Laravel\Scout\Builder;
use Lorisleiva\Actions\ActionRequest;
class InvoiceIndexAction
@ -17,11 +17,11 @@ class InvoiceIndexAction
/**
* @return LengthAwarePaginator<Invoice>
* @return Builder<Invoice>
*/
public function handle(InvoiceFilterScope $filter): LengthAwarePaginator
public function handle(InvoiceFilterScope $filter): Builder
{
return Invoice::withFilter($filter)->with('positions')->paginate(15);
return $filter->getQuery()->query(fn ($q) => $q->with('positions'));
}
public function asController(ActionRequest $request): Response
@ -32,7 +32,7 @@ class InvoiceIndexAction
$filter = InvoiceFilterScope::fromRequest($request->input('filter', ''));
return Inertia::render('invoice/Index', [
'data' => InvoiceResource::collection($this->handle($filter)),
'data' => InvoiceResource::collection($this->handle($filter)->paginate(15)),
]);
}
}

View File

@ -8,27 +8,17 @@ use Lorisleiva\Actions\ActionRequest;
class InvoiceSettings extends LocalSettings implements Storeable
{
public string $from_long;
public string $from;
public string $mobile;
public string $email;
public string $website;
public string $address;
public string $place;
public string $zip;
public string $iban;
public string $bic;
public int $rememberWeeks;
public ?string $from_long;
public ?string $from;
public ?string $mobile;
public ?string $email;
public ?string $website;
public ?string $address;
public ?string $place;
public ?string $zip;
public ?string $iban;
public ?string $bic;
public ?int $rememberWeeks;
public static function group(): string
{

View File

@ -17,12 +17,14 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Laravel\Scout\Searchable;
use stdClass;
class Invoice extends Model
{
/** @use HasFactory<InvoiceFactory> */
use HasFactory;
use Searchable;
public $guarded = [];
@ -120,15 +122,6 @@ class Invoice extends Model
->where('last_remembered_at', '<=', now()->subWeeks($weeks));
}
/**
* @param Builder<self> $query
* @return Builder<self>
*/
public function scopeWithFilter(Builder $query, InvoiceFilterScope $filter): Builder
{
return $filter->apply($query);
}
public function getMailRecipient(): stdClass
{
return (object) [
@ -154,4 +147,20 @@ class Invoice extends Model
]);
}
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
return [
'to' => implode(', ', $this->to),
'usage' => $this->usage,
'greeting' => $this->greeting,
'mail_email' => $this->mail_email,
'status' => $this->status->value,
];
}
}

View File

@ -5,42 +5,43 @@ namespace App\Invoice\Scopes;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\Models\Invoice;
use App\Lib\Filter;
use Illuminate\Database\Eloquent\Builder;
use App\Lib\ScoutFilter;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
/**
* @extends Filter<Invoice>
* @extends ScoutFilter<Invoice>
*/
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class InvoiceFilterScope extends Filter
class InvoiceFilterScope extends ScoutFilter
{
/**
* @param array<int, string> $statuses
*/
public function __construct(
public ?array $statuses = null,
public ?string $search = null
) {
}
/**
* @inheritdoc
*/
public function apply(Builder $query): Builder
{
$query = $query->whereIn('status', $this->statuses);
return $query;
}
/**
* @inheritdoc
*/
public function toDefault(): self
{
$this->statuses = $this->statuses === null ? InvoiceStatus::defaultVisibleValues()->toArray() : $this->statuses;
return $this;
}
/**
* @inheritdoc
*/
public function getQuery(): Builder
{
$this->search = $this->search ?: '';
return Invoice::search($this->search, function ($engine, string $query, array $options) {
if (empty($this->statuses)) {
$filter = 'status = "asa6aeruuni4BahC7Wei6ahm1"';
} else {
$filter = collect($this->statuses)->map(fn (string $status) => "status = \"$status\"")->join(' OR ');
}
return $engine->search($query, [...$options, 'filter' => $filter]);
});
}
}

20
bin/run
View File

@ -1,5 +1,17 @@
#!/bin/bash
set -e
FILESHARE=false
WEB=false
while getopts f opt; do
case $opt in
f) FILESHARE=true ;;
w) WEB=true ;;
esac
done
tmux new-session -d -s test
tmux send-keys -t test "a serve" Enter
@ -12,11 +24,11 @@ tmux send-keys -t test "SS start docker && duu socketi" Enter
tmux new-window -t test
tmux send-keys -t test "nrh" Enter
tmux new-window -t test
tmux send-keys -t test "ggwdk && cd plugins/silva/adrema/assets/vendor/adrema-form && nrd" Enter
$WEB && tmux new-window -t test
$WEB && tmux send-keys -t test "ggwdk && cd plugins/silva/adrema/assets/vendor/adrema-form && nrd" Enter
tmux new-window -t test
tmux send-keys -t test "cd tests/Fileshare && docker compose up" Enter
$FILESHARE && tmux new-window -t test
$FILESHARE && tmux send-keys -t test "cd tests/Fileshare && docker compose up" Enter
tmux attach-session -t test

View File

@ -1,6 +1,7 @@
<?php
use App\Form\Models\Form;
use App\Invoice\Models\Invoice;
use App\Member\Member;
return [
@ -153,7 +154,16 @@ return [
'pagination' => [
'maxTotalHits' => 1000000,
]
]
],
Invoice::class => [
'filterableAttributes' => ['to', 'usage', 'greeting', 'mail_email', 'status', 'id'],
'searchableAttributes' => ['to', 'usage', 'greeting', 'mail_email', 'status', 'id'],
'sortableAttributes' => [],
'displayedAttributes' => ['to', 'usage', 'greeting', 'mail_email', 'status', 'id'],
'pagination' => [
'maxTotalHits' => 1000000,
]
],
],
],

View File

@ -7,6 +7,7 @@ use App\Form\Models\Form;
use App\Lib\Editor\Condition;
use Database\Factories\Traits\FakesMedia;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
use Tests\Feature\Form\FormtemplateFieldRequest;
use Tests\Feature\Form\FormtemplateSectionRequest;
use Tests\RequestFactories\EditorRequestFactory;
@ -48,8 +49,8 @@ class FormFactory extends Factory
'config' => ['sections' => []],
'from' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
'to' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
'registration_from' => $this->faker->dateTimeBetween('-2 weeks', 'now')->format('Y-m-d H:i:s'),
'registration_until' => $this->faker->dateTimeBetween('now', '+2 weeks')->format('Y-m-d H:i:s'),
'registration_from' => $this->faker->dateTimeBetween(Carbon::parse('-2 weeks'), now())->format('Y-m-d H:i:s'),
'registration_until' => $this->faker->dateTimeBetween(now(), Carbon::parse('+2 weeks'))->format('Y-m-d H:i:s'),
'mail_top' => EditorRequestFactory::new()->toData(),
'mail_bottom' => EditorRequestFactory::new()->toData(),
'is_active' => true,

View File

@ -0,0 +1,27 @@
<?php
use App\Invoice\Models\Invoice;
use Illuminate\Database\Migrations\Migration;
use Laravel\Scout\Console\SyncIndexSettingsCommand;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Artisan::call(SyncIndexSettingsCommand::class);
foreach (Invoice::get() as $invoice) {
$invoice->searchable();
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -0,0 +1,30 @@
<?php
use App\Form\Actions\UpdateParticipantSearchIndexAction;
use App\Form\Models\Form;
use App\Lib\Sorting;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
foreach (Form::get() as $form) {
UpdateParticipantSearchIndexAction::run($form);
foreach ($form->participants as $participant) {
$participant->searchable();
}
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -56,6 +56,8 @@
></f-singlefile>
<f-text id="from" v-model="single.from" type="date" label="Von" required></f-text>
<f-text id="to" v-model="single.to" type="date" label="Bis" required></f-text>
<f-text id="registration_from" v-model="single.registration_from" type="datetime-local" label="Registrierung von" required></f-text>
<f-text id="registration_until" v-model="single.registration_until" type="datetime-local" label="Registrierung bis" required></f-text>
<f-textarea
id="excerpt"
v-model="single.excerpt"
@ -64,13 +66,15 @@
:rows="5"
required
></f-textarea>
<f-editor id="description" v-model="single.description" name="description" label="Beschreibung" :rows="10" required></f-editor>
</div>
<div v-if="active === 1">
<f-editor id="description" v-model="single.description" name="description" label="Beschreibung" :rows="10" required></f-editor>
</div>
<div v-if="active === 2">
<ui-note class="mt-2"> Sobald sich der erste Teilnehmer für die Veranstaltung angemeldet hat, kann dieses Formular nicht mehr geändert werden. </ui-note>
<form-builder v-model="single.config" :meta="meta"></form-builder>
</div>
<div v-show="active === 2" class="grid grid-cols-[1fr_300px] gap-3">
<div v-show="active === 3" class="grid grid-cols-[1fr_300px] gap-3">
<ui-note class="mt-2 col-span-full">
Hier kannst du die E-Mail anpassen, die nach der Anmeldung an den Teilnehmer verschickt wird.<br />
Es gibt dafür einen ersten E-Mail-Teil und einen zweiten E-Mail-Teil. Dazwischen werden die Daten des Teilnehmers aufgelistet.<br />
@ -108,14 +112,14 @@
</template>
</f-multiplefiles>
</div>
<div v-if="active === 3">
<div v-if="active === 4">
<div class="grid gap-3">
<ui-remote-resource id="export" v-model="single.export.root" label="Haupt-Ordner"></ui-remote-resource>
<f-select id="group_by" v-model="single.export.group_by" :options="allFields" label="Gruppieren nach" name="group_by"></f-select>
<f-select id="to_group_field" v-model="single.export.to_group_field" :options="allFields" label="Nach Gruppe schreiben" name="to_group_field"></f-select>
</div>
</div>
<div v-show="active === 4" class="grid grid-cols-2 gap-3">
<div v-show="active === 5" class="grid grid-cols-2 gap-3">
<f-switch id="needs_prevention" v-model="single.needs_prevention" name="needs_prevention" label="Prävention"></f-switch>
<f-editor
id="prevention_text"
@ -207,7 +211,7 @@ const fileSettingPopup = ref(null);
const active = ref(0);
const activeMailTab = ref(0);
const tabs = [{ title: 'Allgemeines' }, { title: 'Formular' }, { title: 'Bestätigungs-E-Mail' }, { title: 'Export' }, { title: 'Prävention' }];
const tabs = [{ title: 'Allgemeines' }, { title: 'Beschreibung' }, { title: 'Formular' }, { title: 'Bestätigungs-E-Mail' }, { title: 'Export' }, { title: 'Prävention' }];
const mailTabs = [{ title: 'vor Daten' }, { title: 'nach Daten' }];
const allFields = computed(() => {

View File

@ -74,6 +74,7 @@
</ui-popup>
<page-filter breakpoint="xl" :filterable="false">
<template #buttons>
<f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text>
<f-multipleselect
id="statuses"
:options="meta.statuses"
@ -114,6 +115,7 @@
<div class="flex space-x-2">
<ui-action-button tooltip="Anschauen" :href="invoice.links.pdf" class="btn-info" icon="eye" blank></ui-action-button>
<ui-action-button tooltip="Erinnerung anschauen" :href="invoice.links.rememberpdf" class="btn-info" icon="document" blank></ui-action-button>
<ui-action-button tooltip="Als Bezahlt markieren" class="btn-warning" icon="money" blank @click.prevent="markAsPaid(invoice)"></ui-action-button>
<ui-action-button :data-cy="`edit-button-${invoice.id}`" tooltip="Bearbeiten" class="btn-warning" icon="pencil" @click.prevent="edit(invoice)"></ui-action-button>
<ui-action-button tooltip="Löschen" class="btn-danger" icon="trash" @click.prevent="deleting = invoice"></ui-action-button>
</div>
@ -143,4 +145,9 @@ async function sendMassstore() {
await axios.post(meta.value.links['mass-store'], massstore.value);
massstore.value = null;
}
async function markAsPaid(invoice) {
await axios.patch(invoice.links.update, {...invoice, status: 'Rechnung beglichen'});
await reloadPage();
}
</script>

View File

@ -18,15 +18,15 @@
\node[anchor=base west, text width=205.3mm] at (52.9mm,31.0mm) {\bfseries{\large{<<<!!$fromName!!>>>}}}; %Feld: Jugendverband/-Gruppe
\node[anchor=base west, text width=215.9mm] at (41.3mm,38.8mm) {\bfseries{\large{<<<!!$eventName!!>>>}}}; %Feld: Art der Maßnahme
\node[anchor=base west, text width=104.8mm] at (17.5mm,47.0mm) {\bfseries{\large{<<<!!$zipLocation!!>>>, <<<!!$countryName!!>>>}}};
\node[anchor=base west, text width=41.3mm, align=center] at (170.7mm,47.0mm) {\bfseries{\large{<<<!!$dateFromHuman()!!>>>}}};
\node[anchor=base west, text width=38.4mm, align=center] at (219.9mm,47.0mm) {\bfseries{\large{<<<!!$dateUntilHuman()!!>>>}}};
\node[anchor=base west, text width=41.3mm, align=center] at (170.7mm,47.0mm) {\bfseries{\large{<<<!!$niceDateFrom()!!>>>}}};
\node[anchor=base west, text width=38.4mm, align=center] at (219.9mm,47.0mm) {\bfseries{\large{<<<!!$niceDateUntil()!!>>>}}};
@foreach($chunk as $j => $member)
\node[anchor=base, text width=4mm, align=center] at ($(8.0mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$memberShort($member)>>>};
\node[anchor=base, text width=4mm, align=center] at ($(8.0mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$member->isLeader ? 'L' : ''>>>};
\node[anchor=base, text width=6mm, align=center] at ($(13.0mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$j+1>>>};
\node[anchor=base, text width=67.5mm, align=center] at ($(50.65mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$memberName($member)>>>};
\node[anchor=base, text width=14.6mm, align=center] at ($(92.2mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$memberAge($member)>>>};
\node[anchor=base, text width=79.4mm, align=center] at ($(139.7mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$memberAddress($member)>>>};
\node[anchor=base, text width=67.5mm, align=center] at ($(50.65mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$member->separatedName()>>>};
\node[anchor=base, text width=14.6mm, align=center] at ($(92.2mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$member->age()>>>};
\node[anchor=base, text width=79.4mm, align=center] at ($(139.7mm, 69.0mm + 8.05mm * <<<$j%15>>>)$) {<<<$member->fullAddress()>>>};
@endforeach
\node[anchor=base, text width=23.0mm, align=center] at (278.2mm,196.1mm) {Seite <<<!!$i + 1!!>>> von <<<!!$pages!!>>>};

View File

@ -23,11 +23,11 @@
@foreach($chunk as $i => $member)
\node[anchor=base, text width=7.75mm, align=center] at ($(16.35mm, 76.6mm + 7mm * <<<$i % 17>>>)$) {<<<$i+1>>>};
\node[anchor=base, text width=18mm, align=center] at ($(32.55mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberShort($member)>>>};
\node[anchor=base, text width=70mm, align=center] at ($(80.25mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberName($member)>>>};
\node[anchor=base, text width=118mm, align=center] at ($(178.25mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberAddress($member)>>>};
\node[anchor=base, text width=16mm, align=center] at ($(249.50mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberGender($member)>>>};
\node[anchor=base, text width=16mm, align=center] at ($(269.50mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberAge($member)>>>};
\node[anchor=base, text width=18mm, align=center] at ($(32.55mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$member->isLeader ? 'L' : ''>>>};
\node[anchor=base, text width=70mm, align=center] at ($(80.25mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$member->separatedName()>>>};
\node[anchor=base, text width=118mm, align=center] at ($(178.25mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$member->fullAddress()>>>};
\node[anchor=base, text width=16mm, align=center] at ($(249.50mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$member->genderLetter()>>>};
\node[anchor=base, text width=16mm, align=center] at ($(269.50mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$member->age()>>>};
@endforeach
\end{tikzpicture}

View File

@ -0,0 +1,39 @@
\documentclass[a4paper,landscape]{article}
\usepackage[landscape,top=0cm,left=0cm,bottom=0cm,right=0cm]{geometry}
\usepackage{tikz}
\usepackage{background}
\usepackage{blindtext}
\usetikzlibrary{matrix, shapes.misc, calc}
\pagestyle{empty}
\setlength{\parindent}{0cm}
\backgroundsetup{scale = 1, angle = 0, opacity = 1, color=black, contents = {\includegraphics[width = \paperwidth, height = \paperheight] {wuppertal.pdf}}}
\begin{document}
\noindent \sffamily
@foreach($members as $chunk)
\begin{tikzpicture}[remember picture,overlay,yscale=-1]
\node[anchor=base west] at (17mm,29.62mm) {\bfseries{\small{<<<$niceDateFrom>>>}}};
\node[anchor=base west] at (34mm,29.62mm) {\bfseries{\small{<<<$niceDateUntil>>>}}};
\node[] at (203mm,15.55mm) {\bfseries{\small{<<<$zipLocation>>>}}};
@foreach($chunk as $i => $member)
\node[anchor=center, text width=9.75mm, align=center] at ($(19.35mm, 59.7mm + 9.2mm * <<<$i%14>>>)$) {<<<$i+1>>>};
\node[anchor=center, text width=41.75mm, align=center] at ($(48.35mm, 59.7mm + 9.2mm * <<<$i%14>>>)$) {<<<$member->lastname>>>};
\node[anchor=center, text width=37.75mm, align=center] at ($(91.35mm, 59.7mm + 9.2mm * <<<$i%14>>>)$) {<<<$member->firstname>>>};
\node[anchor=center, text width=45.75mm, align=center] at ($(135.35mm, 59.7mm + 9.2mm * <<<$i%14>>>)$) {<<<$member->address>>>};
\node[anchor=center, text width=26.75mm, align=center] at ($(174.35mm, 59.7mm + 9.2mm * <<<$i%14>>>)$) {<<<$member->city()>>>};
\node[anchor=center, text width=19.75mm, align=center] at ($(199.35mm, 59.7mm + 9.2mm * <<<$i%14>>>)$) {<<<$member->birthdayHuman()>>>};
\node[anchor=center, text width=7.75mm, align=center] at ($(216.35mm, 59.7mm + 9.2mm * <<<$i%14>>>)$) {<<<$member->genderLetter()>>>};
\node[anchor=center, text width=7.75mm, align=center] at ($(276.35mm, 59.7mm + 9.2mm * <<<$i%14>>>)$) {<<<$member->isLeader ? 'GL' : 'T'>>>};
@endforeach
\end{tikzpicture}
\pagebreak
@endforeach
\end{document}

View File

@ -1,22 +0,0 @@
<?php
namespace Tests;
use Illuminate\Contracts\Console\Kernel;
trait CreatesApplication
{
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
}

View File

@ -3,14 +3,12 @@
namespace Tests\EndToEnd\Form;
use App\Form\Models\Form;
use App\Membership\TestersBlock;
use App\Subactivity;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Tests\Feature\Form\FormtemplateSectionRequest;
use Tests\RequestFactories\EditorRequestFactory;
use Tests\TestCase;
uses(FormTestCase::class);
uses(DatabaseTransactions::class);
@ -42,11 +40,24 @@ it('testItDisplaysForms', function () {
->assertJsonPath('data.0.dates', '05.05.2023 - 07.06.2023')
->assertJsonPath('data.0.from_human', '05.05.2023')
->assertJsonPath('data.0.to_human', '07.06.2023')
->assertJsonPath('data.0.can_register', true)
->assertJsonPath('meta.per_page', 15)
->assertJsonPath('meta.base_url', url(''))
->assertJsonPath('meta.total', 1);
});
it('displays registration not possible', function () {
Storage::fake('temp');
$this->loginNami()->withoutExceptionHandling();
Form::factory()
->registrationFrom(now()->addDay())
->withImage('headerImage', 'lala-2.jpg')
->create();
sleep(1);
$this->get('/api/form?perPage=15')->assertJsonPath('data.0.can_register', false);
});
it('testItDisplaysDefaultValueOfField', function () {
Storage::fake('temp');
$this->loginNami()->withoutExceptionHandling();

View File

@ -80,20 +80,23 @@ it('testItShowsEmptyFilters', function () {
$this->callFilter('form.participant.index', ['data' => []], ['form' => $form])->assertJsonPath('meta.filter.data.check', ParticipantFilterScope::$nan);
});
it('sorts by active colums sorting by default', function () {
it('sorts by active colums sorting by default', function (array $sorting, string $by, bool $direction) {
$this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->fields([
$this->checkboxField('check'),
$this->checkboxField('vorname'),
])->create();
$form->update(['meta' => ['active_columns' => [], 'sorting' => ['by' => 'vorname', 'direction' => true]]]);
$form->update(['meta' => ['active_columns' => [], 'sorting' => $sorting]]);
sleep(2);
$this->callFilter('form.participant.index', [], ['form' => $form])
->assertOk()
->assertJsonPath('meta.filter.sort.by', 'vorname')
->assertJsonPath('meta.filter.sort.direction', true);
});
->assertJsonPath('meta.filter.sort.by', $by)
->assertJsonPath('meta.filter.sort.direction', $direction);
})->with([
[['by' => 'vorname', 'direction' => true], 'vorname', true],
[['by' => 'created_at', 'direction' => true], 'created_at', true],
]);
it('testItDisplaysHasNamiField', function () {

View File

@ -1,6 +1,6 @@
<?php
namespace Tests\Feature\Invoice;
namespace Tests\Feature\EndToEnd;
use App\Invoice\BillKind;
use App\Invoice\Enums\InvoiceStatus;
@ -9,8 +9,11 @@ use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
use App\Payment\Subscription;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\EndToEndTestCase;
use Tests\Feature\Invoice\ReceiverRequestFactory;
uses(DatabaseTransactions::class);
uses(EndToEndTestCase::class);
it('testItDisplaysInvoices', function () {
$this->login()->loginNami()->withoutExceptionHandling();
@ -25,6 +28,7 @@ it('testItDisplaysInvoices', function () {
->status(InvoiceStatus::SENT)
->create(['usage' => 'Usa', 'mail_email' => 'a@b.de']);
sleep(2);
test()->get(route('invoice.index'))
->assertInertiaPath('data.data.0.to.name', 'Familie Blabla')
->assertInertiaPath('data.data.0.id', $invoice->id)
@ -78,19 +82,35 @@ it('testValuesCanBeNull', function () {
test()->login()->loginNami()->withoutExceptionHandling();
Invoice::factory()->create();
sleep(2);
test()->get(route('invoice.index'))
->assertInertiaPath('data.data.0.sent_at_human', '');
});
it('testItFiltersForInvoiceStatus', function () {
it('filters for invoice status', function (array $filter, int $count) {
test()->login()->loginNami()->withoutExceptionHandling();
Invoice::factory()->status(InvoiceStatus::NEW)->create();
Invoice::factory()->status(InvoiceStatus::SENT)->count(2)->create();
Invoice::factory()->status(InvoiceStatus::PAID)->count(3)->create();
test()->callFilter('invoice.index', [])->assertInertiaCount('data.data', 3);
test()->callFilter('invoice.index', ['statuses' => []])->assertInertiaCount('data.data', 0);
test()->callFilter('invoice.index', ['statuses' => ['Neu']])->assertInertiaCount('data.data', 1);
test()->callFilter('invoice.index', ['statuses' => ['Neu', 'Rechnung beglichen']])->assertInertiaCount('data.data', 4);
test()->callFilter('invoice.index', ['statuses' => ['Neu', 'Rechnung beglichen', 'Rechnung gestellt']])->assertInertiaCount('data.data', 6);
});
sleep(2);
test()->callFilter('invoice.index', $filter)->assertInertiaCount('data.data', $count);
})->with([
[[], 3],
[['statuses' => []], 0],
[['statuses' => ['Neu']], 1],
[['statuses' => ['Neu', 'Rechnung beglichen']], 4],
[['statuses' => ['Neu', 'Rechnung beglichen', 'Rechnung gestellt']], 6],
]);
it('searches invoice usage', function (array $filter, int $count) {
test()->login()->loginNami()->withoutExceptionHandling();
Invoice::factory()->status(InvoiceStatus::NEW)->create(['usage' => 'Kein Zweck']);
Invoice::factory()->status(InvoiceStatus::NEW)->create(['usage' => 'Mitgliedsbeitrag']);
sleep(2);
test()->callFilter('invoice.index', $filter)->assertInertiaCount('data.data', $count);
})->with([
[['search' => 'Mitgliedsbeitrag'], 1],
[['search' => 'Kein'], 1],
]);

View File

@ -3,6 +3,7 @@
namespace Tests;
use App\Form\Models\Form;
use App\Invoice\Models\Invoice;
use App\Member\Member;
use Laravel\Scout\Console\FlushCommand;
use Laravel\Scout\Console\SyncIndexSettingsCommand;
@ -26,6 +27,7 @@ abstract class EndToEndTestCase extends TestCase
config()->set('scout.driver', 'meilisearch');
Artisan::call(FlushCommand::class, ['model' => Member::class]);
Artisan::call(FlushCommand::class, ['model' => Form::class]);
Artisan::call(FlushCommand::class, ['model' => Invoice::class]);
Artisan::call(SyncIndexSettingsCommand::class);
return $this;

View File

@ -2,211 +2,202 @@
namespace Tests\Feature\Contribution;
use App\Contribution\Documents\ContributionDocument;
use App\Contribution\Documents\RdpNrwDocument;
use App\Contribution\Documents\CitySolingenDocument;
use App\Contribution\Documents\WuppertalDocument;
use App\Country;
use App\Gender;
use App\Invoice\InvoiceSettings;
use App\Member\Member;
use Generator;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\Passport\Client;
use Laravel\Passport\Passport;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\RequestFactories\ContributionMemberApiRequestFactory;
use Tests\RequestFactories\ContributionRequestFactory;
use Tests\TestCase;
use Zoomyboy\Tex\Tex;
class StoreTest extends TestCase
{
use DatabaseTransactions;
uses(DatabaseTransactions::class);
/**
* @testWith ["App\\Contribution\\Documents\\CitySolingenDocument", ["Super tolles Lager", "Max Muster", "Jane Muster", "15.06.1991"]]
* ["App\\Contribution\\Documents\\RdpNrwDocument", ["Muster, Max", "Muster, Jane", "15.06.1991", "42777 SG"]]
* ["App\\Contribution\\Documents\\CityRemscheidDocument", ["Max", "Muster", "Jane"]]
* ["App\\Contribution\\Documents\\CityFrankfurtMainDocument", ["Max", "Muster", "Jane"]]
* ["App\\Contribution\\Documents\\BdkjHesse", ["Max", "Muster", "Jane"]]
*
* @param array<int, string> $bodyChecks
*/
public function testItCompilesContributionDocumentsViaRequest(string $type, array $bodyChecks): void
{
$this->withoutExceptionHandling();
Tex::spy();
$this->login()->loginNami();
$member1 = Member::factory()->defaults()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Max', 'lastname' => 'Muster']);
$member2 = Member::factory()->defaults()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Jane', 'lastname' => 'Muster']);
$response = $this->call('GET', '/contribution-generate', [
'payload' => ContributionRequestFactory::new()->type($type)->state([
'dateFrom' => '1991-06-15',
'dateUntil' => '1991-06-16',
'eventName' => 'Super tolles Lager',
'members' => [$member1->id, $member2->id],
'type' => $type,
'zipLocation' => '42777 SG',
])->toBase64(),
]);
$response->assertSessionDoesntHaveErrors();
$response->assertOk();
Tex::assertCompiled($type, fn ($document) => $document->hasAllContent($bodyChecks));
}
public function testItCompilesGroupNameInSolingenDocument(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
Tex::spy();
InvoiceSettings::fake(['from_long' => 'Stamm BiPi']);
$this->call('GET', '/contribution-generate', [
'payload' => ContributionRequestFactory::new()->type(CitySolingenDocument::class)->toBase64(),
]);
Tex::assertCompiled(CitySolingenDocument::class, fn ($document) => $document->hasAllContent(['Stamm BiPi']));
}
public function testItCompilesContributionDocumentsViaApi(): void
{
$this->withoutExceptionHandling();
Tex::spy();
Gender::factory()->female()->create();
Gender::factory()->male()->create();
Passport::actingAsClient(Client::factory()->create(), ['contribution-generate']);
$country = Country::factory()->create();
Member::factory()->defaults()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Max', 'lastname' => 'Muster']);
Member::factory()->defaults()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Jane', 'lastname' => 'Muster']);
$response = $this->postJson('/api/contribution-generate', [
'country' => $country->id,
'dateFrom' => '1991-06-15',
'dateUntil' => '1991-06-16',
'eventName' => 'Super tolles Lager',
'type' => CitySolingenDocument::class,
'zipLocation' => '42777 SG',
'member_data' => [
ContributionMemberApiRequestFactory::new()->create(),
ContributionMemberApiRequestFactory::new()->create(),
],
]);
$response->assertSessionDoesntHaveErrors();
$response->assertOk();
Tex::assertCompiled(CitySolingenDocument::class, fn ($document) => $document->hasAllContent(['Super']));
}
/**
* @testWith [""]
* ["aaaa"]
* ["YWFhCg=="]
*/
public function testInputShouldBeBase64EncodedJson(string $payload): void
{
$this->login()->loginNami();
$this->call('GET', '/contribution-generate', ['payload' => $payload])->assertSessionHasErrors('payload');
}
/**
* @param array<string, string> $input
* @param class-string<ContributionDocument> $documentClass
*/
#[DataProvider('validationDataProvider')]
public function testItValidatesInput(array $input, string $documentClass, string $errorField): void
{
$this->login()->loginNami();
Country::factory()->create();
Member::factory()->defaults()->create();
$this->postJson('/contribution-validate', ContributionRequestFactory::new()->type($documentClass)->state($input)->create())
->assertJsonValidationErrors($errorField);
}
/**
* @param array<string, string> $input
* @param class-string<ContributionDocument> $documentClass
*/
#[DataProvider('validationDataProvider')]
public function testItValidatesInputBeforeGeneration(array $input, string $documentClass, string $errorField): void
{
$this->login()->loginNami();
Country::factory()->create();
Member::factory()->defaults()->create();
$this->call('GET', '/contribution-generate', [
'payload' => ContributionRequestFactory::new()->type($documentClass)->state($input)->toBase64(),
])->assertSessionHasErrors($errorField);
}
public static function validationDataProvider(): Generator
{
yield [
dataset('validation', function () {
return [
[
['type' => 'aaa'],
CitySolingenDocument::class,
'type',
];
yield [
],
[
['type' => ''],
CitySolingenDocument::class,
'type',
];
yield [
],
[
['dateFrom' => ''],
CitySolingenDocument::class,
'dateFrom',
];
yield [
],
[
['dateFrom' => '2022-01'],
CitySolingenDocument::class,
'dateFrom',
];
yield [
],
[
['dateUntil' => ''],
CitySolingenDocument::class,
'dateUntil',
];
yield [
],
[
['dateUntil' => '2022-01'],
CitySolingenDocument::class,
'dateUntil',
];
yield [
],
[
['country' => -1],
RdpNrwDocument::class,
'country',
];
yield [
],
[
['country' => 'AAAA'],
RdpNrwDocument::class,
'country',
];
yield [
],
[
['members' => 'A'],
RdpNrwDocument::class,
'members',
];
yield [
],
[
['members' => [99999]],
RdpNrwDocument::class,
'members.0',
];
yield [
],
[
['members' => ['lalala']],
RdpNrwDocument::class,
'members.0',
];
yield [
],
[
['eventName' => ''],
CitySolingenDocument::class,
'eventName',
];
yield [
],
[
['zipLocation' => ''],
CitySolingenDocument::class,
'zipLocation',
];
}
}
],
[
['zipLocation' => ''],
WuppertalDocument::class,
'zipLocation',
],
[
['dateFrom' => ''],
WuppertalDocument::class,
'dateFrom',
],
[
['dateUntil' => ''],
WuppertalDocument::class,
'dateUntil',
],
];
});
it('compiles documents via api', function (string $type, array $bodyChecks) {
$this->withoutExceptionHandling();
Tex::spy();
$this->login()->loginNami();
$member1 = Member::factory()->defaults()->male()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Max', 'lastname' => 'Muster']);
$member2 = Member::factory()->defaults()->female()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Jane', 'lastname' => 'Muster']);
$response = $this->call('GET', '/contribution-generate', [
'payload' => ContributionRequestFactory::new()->type($type)->state([
'dateFrom' => '1991-06-15',
'dateUntil' => '1991-06-16',
'eventName' => 'Super tolles Lager',
'members' => [$member1->id, $member2->id],
'type' => $type,
'zipLocation' => '42777 SG',
])->toBase64(),
]);
$response->assertSessionDoesntHaveErrors();
$response->assertOk();
Tex::assertCompiled($type, fn ($document) => $document->hasAllContent($bodyChecks));
})->with([
["App\\Contribution\\Documents\\CitySolingenDocument", ["Super tolles Lager", "Max Muster", "Jane Muster", "15.06.1991"]],
["App\\Contribution\\Documents\\RdpNrwDocument", ["Muster, Max", "Muster, Jane", "15.06.1991", "42777 SG"]],
["App\\Contribution\\Documents\\CityRemscheidDocument", ["Max", "Muster", "Jane"]],
["App\\Contribution\\Documents\\CityFrankfurtMainDocument", ["Max", "Muster", "Jane"]],
["App\\Contribution\\Documents\\BdkjHesse", ["Max", "Muster", "Jane"]],
["App\\Contribution\\Documents\\WuppertalDocument", ["Max", "Muster", "Jane", "42777 SG", "15.06.1991", "16.06.1991"]],
]);
it('testItCompilesGroupNameInSolingenDocument', function () {
$this->withoutExceptionHandling()->login()->loginNami();
Tex::spy();
InvoiceSettings::fake(['from_long' => 'Stamm BiPi']);
$this->call('GET', '/contribution-generate', [
'payload' => ContributionRequestFactory::new()->type(CitySolingenDocument::class)->toBase64(),
]);
Tex::assertCompiled(CitySolingenDocument::class, fn ($document) => $document->hasAllContent(['Stamm BiPi']));
});
it('testItCompilesContributionDocumentsViaApi', function () {
$this->withoutExceptionHandling();
Tex::spy();
Gender::factory()->female()->create();
Gender::factory()->male()->create();
Passport::actingAsClient(Client::factory()->create(), ['contribution-generate']);
$country = Country::factory()->create();
Member::factory()->defaults()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Max', 'lastname' => 'Muster']);
Member::factory()->defaults()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Jane', 'lastname' => 'Muster']);
$response = $this->postJson('/api/contribution-generate', [
'country' => $country->id,
'dateFrom' => '1991-06-15',
'dateUntil' => '1991-06-16',
'eventName' => 'Super tolles Lager',
'type' => CitySolingenDocument::class,
'zipLocation' => '42777 SG',
'member_data' => [
ContributionMemberApiRequestFactory::new()->create(),
ContributionMemberApiRequestFactory::new()->create(),
],
]);
$response->assertSessionDoesntHaveErrors();
$response->assertOk();
Tex::assertCompiled(CitySolingenDocument::class, fn ($document) => $document->hasAllContent(['Super']));
});
it('testInputShouldBeBase64EncodedJson', function (string $payload) {
$this->login()->loginNami();
$this->call('GET', '/contribution-generate', ['payload' => $payload])->assertSessionHasErrors('payload');
})->with([
[""],
["aaaa"],
["YWFhCg=="],
]);
it('testItValidatesInput', function (array $input, string $documentClass, string $errorField) {
$this->login()->loginNami();
Country::factory()->create();
Member::factory()->defaults()->create();
$this->postJson('/contribution-validate', ContributionRequestFactory::new()->type($documentClass)->state($input)->create())
->assertJsonValidationErrors($errorField);
})->with('validation');
it('testItValidatesInputBeforeGeneration', function (array $input, string $documentClass, string $errorField) {
$this->login()->loginNami();
Country::factory()->create();
Member::factory()->defaults()->create();
$this->call('GET', '/contribution-generate', [
'payload' => ContributionRequestFactory::new()->type($documentClass)->state($input)->toBase64(),
])->assertSessionHasErrors($errorField);
})->with('validation');

View File

@ -55,6 +55,22 @@ class FormRegisterActionTest extends FormTestCase
$this->assertEquals('Abraham', $participants->first()->data['spitzname']);
}
public function testItCannotRegisterWhenRegistrationFromReached(): void
{
$this->login()->loginNami();
$form = Form::factory()->registrationFrom(now()->addDay())->create();
$this->register($form, [])->assertJsonValidationErrors(['event' => 'Anmeldung zzt nicht möglich.']);
}
public function testItCannotRegisterWhenRegistrationUntilReached(): void
{
$this->login()->loginNami();
$form = Form::factory()->registrationUntil(now()->subDay())->create();
$this->register($form, [])->assertJsonValidationErrors(['event' => 'Anmeldung zzt nicht möglich.']);
}
public function testItSendsEmailToParticipant(): void
{
$this->login()->loginNami()->withoutExceptionHandling();

View File

@ -5,8 +5,6 @@ namespace Tests\Feature\Form;
use App\Fileshare\Data\FileshareResourceData;
use App\Form\Data\ExportData;
use App\Form\Models\Form;
use App\Lib\Editor\Condition;
use App\Lib\Editor\EditorData;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\EditorRequestFactory;
@ -31,6 +29,20 @@ class FormUpdateActionTest extends FormTestCase
$this->assertTrue($form->config->sections->get(0)->fields->get(0)->maxToday);
}
public function testItSetsRegistrationDates(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->create();
$payload = FormRequest::new()->registrationFrom('2023-05-04 01:00:00')->registrationUntil('2023-07-07 01:00:00')->create();
$this->patchJson(route('form.update', ['form' => $form]), $payload)->assertOk();
$form = $form->fresh();
$this->assertEquals('2023-05-04 01:00', $form->registration_from->format('Y-m-d H:i'));
$this->assertEquals('2023-07-07 01:00', $form->registration_until->format('Y-m-d H:i'));
}
public function testItSetsTexts(): void
{
$this->login()->loginNami()->withoutExceptionHandling();

View File

@ -21,7 +21,6 @@ use Zoomyboy\TableDocument\TestsExcelDocuments;
class TestCase extends BaseTestCase
{
use CreatesApplication;
use TestsInertia;
use MakesHttpCalls;
use TestsExcelDocuments;