Compare commits

..

No commits in common. "master" and "memberships" have entirely different histories.

134 changed files with 2665 additions and 4352 deletions

View File

@ -1,52 +1,5 @@
# Letzte Änderungen
### 1.12.23
- Veranstaltungs-Teilnehmer*innen können nun abgemeldet statt vollständig gelöscht werden.
- Beim Excel-Export wird eine Spalte "ID" angezeigt mit der ID des TNs
### 1.12.22
- Bei Mitgliedern wird nun auch die geschäftliche tel-nr aktualisiert
### 1.12.21
- Reply-To-Header bei Versand von Prävention-und-Veranstaltungs-Mails kann nun eingestellt werden
### 1.12.20
- Nachnelde-Link kann nun erstellt werden für Veranstaltungen
### 1.12.19
- Zuschusslisten können nun aus Veranstaltungs-Daten erstellt werden
- Veranstaltungs-Übersicht zeigt nun Tags an
### 1.12.18
- Fix: Initialisierung klappt nun auch, wenn Mitgliedsnummer mit einer 0 beginnt
### 1.12.17
- Fix: Mitgliedschaften werden beim Sammel-Speichern nicht mehr doppelt angelegt
### 1.12.16
- Mitgliedschaften können nun bei Mitgliedschaften-Übersicht gelöscht werden
### 1.12.15
- Bestätigung wird eingeblendet beim Kopieren eines Events
### 1.12.14
- Bilder werden nun mitkopiert beim Kopieren eines Events
### 1.12.13
- Kopieren von bestehenden Veranstaltungen
- Präventions-Erinnerung automatisch versenden
### 1.12.11
- Fix: Bank Account mit abrufen wenn Mitglied editiert wird

View File

@ -2,11 +2,11 @@
namespace App\Contribution\Actions;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\ContributionFactory;
use App\Contribution\Requests\GenerateRequest;
use App\Contribution\Documents\ContributionDocument;
use App\Rules\JsonBase64Rule;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
@ -15,19 +15,23 @@ class GenerateAction
{
use AsAction;
public function handle(HasContributionData $request): BaseCompiler
/**
* @param class-string<ContributionDocument> $document
* @param array<string, mixed> $payload
*/
public function handle(string $document, array $payload): BaseCompiler
{
return Tex::compile($request->type()::fromPayload($request));
return Tex::compile($document::fromRequest($payload));
}
public function asController(GenerateRequest $request): BaseCompiler|JsonResponse
public function asController(ActionRequest $request): BaseCompiler
{
app(ContributionFactory::class)->validateType($request);
$request->validateContribution();
$payload = $this->payload($request);
$type = data_get($payload, 'type');
ValidateAction::validateType($type);
Validator::make($payload, app(ContributionFactory::class)->rules($type))->validate();
return $request->input('validate')
? response()->json([])
: $this->handle($request);
return $this->handle($type, $payload);
}
/**
@ -39,4 +43,12 @@ class GenerateAction
'payload' => [new JsonBase64Rule()],
];
}
/**
* @return array<string, string>
*/
private function payload(ActionRequest $request): array
{
return json_decode(rawurldecode(base64_decode($request->input('payload', ''))), true);
}
}

View File

@ -2,9 +2,8 @@
namespace App\Contribution\Actions;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\ContributionFactory;
use App\Contribution\Requests\GenerateApiRequest;
use App\Contribution\Documents\ContributionDocument;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
@ -14,17 +13,26 @@ class GenerateApiAction
use AsAction;
/**
* @todo merge this with GenerateAction
* @param class-string<ContributionDocument> $document
* @param array<string, mixed> $payload
*/
public function handle(HasContributionData $request): BaseCompiler
public function handle(string $document, array $payload): BaseCompiler
{
return Tex::compile($request->type()::fromPayload($request));
return Tex::compile($document::fromApiRequest($payload));
}
public function asController(GenerateApiRequest $request): BaseCompiler
public function asController(ActionRequest $request): BaseCompiler
{
app(ContributionFactory::class)->validateType($request);
ValidateAction::validateType($request->input('type'));
return $this->handle($request);
return $this->handle($request->input('type'), $request->input());
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [];
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Contribution\Actions;
use App\Contribution\ContributionFactory;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ValidateAction
{
use AsAction;
public function asController(): JsonResponse
{
return response()->json(['valid' => true]);
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return app(ContributionFactory::class)->rules(request()->type);
}
public function prepareForValidation(ActionRequest $request): void
{
static::validateType($request->input('type'));
}
public static function validateType(?string $type = null): void
{
Validator::make(['type' => $type], app(ContributionFactory::class)->typeRule())->validate();
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Contribution\Contracts;
use App\Contribution\Data\MemberData;
use Carbon\Carbon;
use App\Contribution\Documents\ContributionDocument;
use App\Country;
use Illuminate\Support\Collection;
interface HasContributionData {
public function dateFrom(): Carbon;
public function dateUntil(): Carbon;
public function zipLocation(): string;
public function eventName(): string;
/**
* @return class-string<ContributionDocument>
*/
public function type(): string;
/**
* @return Collection<int, MemberData>
*/
public function members(): Collection;
public function country(): ?Country;
public function validateContribution(): void;
}

View File

@ -2,7 +2,6 @@
namespace App\Contribution;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\Documents\BdkjHesse;
use App\Contribution\Documents\ContributionDocument;
use App\Contribution\Documents\RdpNrwDocument;
@ -11,7 +10,6 @@ use App\Contribution\Documents\CitySolingenDocument;
use App\Contribution\Documents\CityFrankfurtMainDocument;
use App\Contribution\Documents\WuppertalDocument;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class ContributionFactory
@ -29,13 +27,13 @@ class ContributionFactory
];
/**
* @return Collection<int, array{name: string, id: class-string<ContributionDocument>}>
* @return Collection<int, array{title: string, class: class-string<ContributionDocument>}>
*/
public function compilerSelect(): Collection
{
return collect($this->documents)->map(fn ($document) => [
'name' => $document::getName(),
'id' => $document,
'title' => $document::buttonName(),
'class' => $document,
]);
}
@ -61,9 +59,4 @@ class ContributionFactory
...$type::rules(),
];
}
public function validateType(HasContributionData $request): void {
Validator::make(['type' => $request->type()], $this->typeRule())->validate();
}
}

View File

@ -47,7 +47,7 @@ class MemberData extends Data
return collect($data)->map(fn ($member) => self::factory()->withoutMagicalCreation()->from([
...$member,
'birthday' => Carbon::parse($member['birthday'])->toAtomString(),
'gender' => $member['gender'] ? Gender::fromString($member['gender']) : null,
'gender' => Gender::fromString($member['gender']),
'isLeader' => $member['is_leader'],
]));
}

View File

@ -2,11 +2,9 @@
namespace App\Contribution\Documents;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use App\Form\Enums\SpecialType;
use Carbon\Carbon;
use Illuminate\Support\Collection;
@ -19,8 +17,8 @@ class BdkjHesse extends ContributionDocument
* @param Collection<int, Collection<int, MemberData>> $members
*/
public function __construct(
public Carbon $dateFrom,
public Carbon $dateUntil,
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
public Collection $members,
@ -41,15 +39,33 @@ class BdkjHesse extends ContributionDocument
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public static function fromPayload(HasContributionData $request): self
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): self
{
return new self(
dateFrom: $request->dateFrom(),
dateUntil: $request->dateUntil(),
zipLocation: $request->zipLocation(),
country: $request->country(),
members: $request->members()->chunk(20),
eventName: $request->eventName(),
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromModels($request['members'])->chunk(20),
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(20),
eventName: $request['eventName'],
);
}
@ -117,15 +133,4 @@ class BdkjHesse extends ContributionDocument
'zipLocation' => 'required|string',
];
}
public static function requiredFormSpecialTypes(): array {
return [
SpecialType::FIRSTNAME,
SpecialType::LASTNAME,
SpecialType::BIRTHDAY,
SpecialType::ZIP,
SpecialType::LOCATION,
SpecialType::GENDER,
];
}
}

View File

@ -2,15 +2,12 @@
namespace App\Contribution\Documents;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use App\Form\Enums\SpecialType;
use App\Invoice\InvoiceSettings;
use Illuminate\Support\Collection;
use Carbon\Carbon;
class CityFrankfurtMainDocument extends ContributionDocument
{
@ -23,8 +20,8 @@ class CityFrankfurtMainDocument extends ContributionDocument
* @param Collection<int, Collection<int, MemberData>> $members
*/
public function __construct(
public Carbon $dateFrom,
public Carbon $dateUntil,
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
public Collection $members,
@ -36,15 +33,33 @@ class CityFrankfurtMainDocument extends ContributionDocument
$this->fromName = app(InvoiceSettings::class)->from_long;
}
public static function fromPayload(HasContributionData $request): self
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): self
{
return new self(
dateFrom: $request->dateFrom(),
dateUntil: $request->dateUntil(),
zipLocation: $request->zipLocation(),
country: $request->country(),
members: $request->members()->chunk(15),
eventName: $request->eventName(),
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromModels($request['members'])->chunk(15),
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(15),
eventName: $request['eventName'],
);
}
@ -75,15 +90,4 @@ class CityFrankfurtMainDocument extends ContributionDocument
'zipLocation' => 'required|string',
];
}
public static function requiredFormSpecialTypes(): array {
return [
SpecialType::FIRSTNAME,
SpecialType::LASTNAME,
SpecialType::BIRTHDAY,
SpecialType::ZIP,
SpecialType::LOCATION,
SpecialType::ADDRESS,
];
}
}

View File

@ -2,14 +2,12 @@
namespace App\Contribution\Documents;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use App\Form\Enums\SpecialType;
use App\Member\Member;
use Illuminate\Support\Collection;
use Carbon\Carbon;
class CityRemscheidDocument extends ContributionDocument
{
@ -21,8 +19,8 @@ class CityRemscheidDocument extends ContributionDocument
* @param Collection<int, Collection<int, Member>> $children
*/
public function __construct(
public Carbon $dateFrom,
public Carbon $dateUntil,
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
public Collection $leaders,
@ -34,18 +32,40 @@ class CityRemscheidDocument extends ContributionDocument
$this->setEventName($eventName);
}
public static function fromPayload(HasContributionData $request): self
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): self
{
[$leaders, $children] = $request->members()->partition(fn ($member) => $member->isLeader);
[$leaders, $children] = MemberData::fromModels($request['members'])->partition(fn ($member) => $member->isLeader);
return new self(
dateFrom: $request->dateFrom(),
dateUntil: $request->dateUntil(),
zipLocation: $request->zipLocation(),
country: $request->country(),
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
leaders: $leaders->values()->toBase()->chunk(6),
children: $children->values()->toBase()->chunk(20),
eventName: $request->eventName(),
eventName: $request['eventName'],
);
}
/**
* {@inheritdoc}
*/
public static function fromApiRequest(array $request): self
{
$members = MemberData::fromApi($request['member_data']);
[$leaders, $children] = $members->partition(fn ($member) => $member->isLeader);
return new self(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
leaders: $leaders->values()->toBase()->chunk(6),
children: $children->values()->toBase()->chunk(20),
eventName: $request['eventName'],
);
}
@ -66,15 +86,4 @@ class CityRemscheidDocument extends ContributionDocument
'country' => 'required|integer|exists:countries,id',
];
}
public static function requiredFormSpecialTypes(): array {
return [
SpecialType::FIRSTNAME,
SpecialType::LASTNAME,
SpecialType::ADDRESS,
SpecialType::BIRTHDAY,
SpecialType::ZIP,
SpecialType::LOCATION,
];
}
}

View File

@ -2,9 +2,7 @@
namespace App\Contribution\Documents;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\Data\MemberData;
use App\Form\Enums\SpecialType;
use App\Invoice\InvoiceSettings;
use Carbon\Carbon;
use Illuminate\Support\Collection;
@ -18,8 +16,8 @@ class CitySolingenDocument extends ContributionDocument
* @param Collection<int, MemberData> $members
*/
final private function __construct(
public Carbon $dateFrom,
public Carbon $dateUntil,
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public Collection $members,
public string $eventName,
@ -32,14 +30,28 @@ class CitySolingenDocument extends ContributionDocument
/**
* {@inheritdoc}
*/
public static function fromPayload(HasContributionData $request): static
public static function fromRequest(array $request): static
{
return new static(
dateFrom: $request->dateFrom(),
dateUntil: $request->dateUntil(),
zipLocation: $request->zipLocation(),
members: $request->members(),
eventName: $request->eventName(),
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
members: MemberData::fromModels($request['members']),
eventName: $request['eventName'],
);
}
/**
* {@inheritdoc}
*/
public static function fromApiRequest(array $request): static
{
return new static(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
members: MemberData::fromApi($request['member_data']),
eventName: $request['eventName'],
);
}
@ -63,6 +75,8 @@ class CitySolingenDocument extends ContributionDocument
public function checkboxes(): string
{
$output = '';
$firstRow = collect(['B' => 'Jugendbildungsmaßnahme', 'G' => 'Gruppenleiter/innenschulung', 'FK' => 'Ferienkolonie', 'F' => 'Freizeitnaßnahme'])->map(function ($item, $key) {
return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox') . '{' . $item . '}';
})->implode(' & ') . ' \\\\';
@ -95,15 +109,4 @@ class CitySolingenDocument extends ContributionDocument
'zipLocation' => 'required|string',
];
}
public static function requiredFormSpecialTypes(): array {
return [
SpecialType::FIRSTNAME,
SpecialType::LASTNAME,
SpecialType::BIRTHDAY,
SpecialType::ZIP,
SpecialType::LOCATION,
SpecialType::ADDRESS,
];
}
}

View File

@ -2,8 +2,6 @@
namespace App\Contribution\Documents;
use App\Contribution\Contracts\HasContributionData;
use App\Form\Enums\SpecialType;
use Zoomyboy\Tex\Document;
use Zoomyboy\Tex\Template;
@ -13,12 +11,15 @@ abstract class ContributionDocument extends Document
abstract public static function getName(): string;
abstract public static function fromPayload(HasContributionData $request): self;
/**
* @param ContributionRequestArray $request
*/
abstract public static function fromRequest(array $request): self;
/**
* @return array<int, SpecialType>
* @param ContributionApiRequestArray $request
*/
abstract public static function requiredFormSpecialTypes(): array;
abstract public static function fromApiRequest(array $request): self;
/**
* @return array<string, mixed>
@ -37,6 +38,11 @@ abstract class ContributionDocument extends Document
];
}
public static function buttonName(): string
{
return 'Für ' . static::getName() . ' erstellen';;
}
public function setEventName(string $eventName): void
{
$this->eventName = $eventName;

View File

@ -2,14 +2,11 @@
namespace App\Contribution\Documents;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use App\Form\Enums\SpecialType;
use Illuminate\Support\Collection;
use Carbon\Carbon;
class RdpNrwDocument extends ContributionDocument
{
@ -20,8 +17,8 @@ class RdpNrwDocument extends ContributionDocument
* @param Collection<int, Collection<int, MemberData>> $members
*/
public function __construct(
public Carbon $dateFrom,
public Carbon $dateUntil,
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
public Collection $members,
@ -32,15 +29,33 @@ class RdpNrwDocument extends ContributionDocument
$this->setEventName($eventName);
}
public static function fromPayload(HasContributionData $request): self
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): self
{
return new self(
dateFrom: $request->dateFrom(),
dateUntil: $request->dateUntil(),
zipLocation: $request->zipLocation(),
country: $request->country(),
members: $request->members()->chunk(17),
eventName: $request->eventName(),
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromModels($request['members'])->chunk(17),
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(17),
eventName: $request['eventName'],
);
}
@ -66,15 +81,4 @@ class RdpNrwDocument extends ContributionDocument
'zipLocation' => 'required|string',
];
}
public static function requiredFormSpecialTypes(): array {
return [
SpecialType::FIRSTNAME,
SpecialType::LASTNAME,
SpecialType::BIRTHDAY,
SpecialType::ZIP,
SpecialType::LOCATION,
SpecialType::GENDER,
];
}
}

View File

@ -2,14 +2,11 @@
namespace App\Contribution\Documents;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use App\Form\Enums\SpecialType;
use Illuminate\Support\Collection;
use Carbon\Carbon;
class WuppertalDocument extends ContributionDocument
{
@ -21,8 +18,8 @@ class WuppertalDocument extends ContributionDocument
* @param Collection<int, Collection<int, MemberData>> $members
*/
public function __construct(
public Carbon $dateFrom,
public Carbon $dateUntil,
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
public Collection $members,
@ -33,15 +30,33 @@ class WuppertalDocument extends ContributionDocument
$this->setEventName($eventName);
}
public static function fromPayload(HasContributionData $request): self
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): self
{
return new self(
dateFrom: $request->dateFrom(),
dateUntil: $request->dateUntil(),
zipLocation: $request->zipLocation(),
country: $request->country(),
members: $request->members()->chunk(14),
eventName: $request->eventName(),
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'],
);
}
@ -61,16 +76,4 @@ class WuppertalDocument extends ContributionDocument
'zipLocation' => 'required|string',
];
}
public static function requiredFormSpecialTypes(): array {
return [
SpecialType::FIRSTNAME,
SpecialType::LASTNAME,
SpecialType::ADDRESS,
SpecialType::BIRTHDAY,
SpecialType::ZIP,
SpecialType::LOCATION,
SpecialType::GENDER,
];
}
}

View File

@ -1,256 +0,0 @@
<?php
namespace App\Contribution\Enums;
enum Country: string {
case AD = 'Andorra';
case AE = 'Vereinigte Arabische Emirate';
case AF = 'Afghanistan';
case AG = 'Antigua und Barbuda';
case AI = 'Anguilla';
case AL = 'Albanien';
case AM = 'Armenien';
case AN = 'Niederländische Antillen';
case AO = 'Angola';
case AQ = 'Antarktis';
case AR = 'Argentinien';
case AS = 'Amerikanisch-Samoa';
case AT = 'Österreich (Austria)';
case AU = 'Australien';
case AW = 'Aruba';
case AZ = 'Azerbaijan';
case BA = 'Bosnien-Herzegovina';
case BB = 'Barbados';
case BD = 'Bangladesh';
case BE = 'Belgien';
case BF = 'Burkina Faso';
case BG = 'Bulgarien';
case BH = 'Bahrain';
case BI = 'Burundi';
case BJ = 'Benin';
case BM = 'Bermudas';
case BN = 'Brunei Darussalam';
case BO = 'Bolivien';
case BR = 'Brasilien';
case BS = 'Bahamas';
case BT = 'Bhutan';
case BV = 'Bouvet Island';
case BW = 'Botswana';
case BY = 'Weißrußland (Belarus)';
case BZ = 'Belize';
case CA = 'Canada';
case CC = 'Cocos (Keeling) Islands';
case CD = 'Demokratische Republik Kongo';
case CF = 'Zentralafrikanische Republik';
case CG = 'Kongo';
case CH = 'Schweiz';
case CI = 'Elfenbeinküste (Cote DIvoire)';
case CK = 'Cook Islands';
case CL = 'Chile';
case CM = 'Kamerun';
case CN = 'China';
case CO = 'Kolumbien';
case CR = 'Costa Rica';
case CS = 'Tschechoslowakei (ehemalige)';
case CU = 'Kuba';
case CV = 'Kap Verde';
case CX = 'Christmas Island';
case CY = 'Zypern';
case CZ = 'Tschechische Republik';
case DE = 'Deutschland';
case DJ = 'Djibouti';
case DK = 'Dänemark';
case DM = 'Dominica';
case DO = 'Dominikanische Republik';
case DZ = 'Algerien';
case EC = 'Ecuador';
case EE = 'Estland';
case EG = 'Ägypten';
case EH = 'Westsahara';
case ER = 'Eritrea';
case ES = 'Spanien';
case ET = 'Äthiopien';
case FI = 'Finnland';
case FJ = 'Fiji';
case FK = 'Falkland-Inseln (Malvinas)';
case FM = 'Micronesien';
case FO = 'Faröer-Inseln';
case FR = 'Frankreich';
case FX = 'France, Metropolitan';
case GA = 'Gabon';
case GD = 'Grenada';
case GE = 'Georgien';
case GF = 'Französisch Guiana';
case GH = 'Ghana';
case GI = 'Gibraltar';
case GL = 'Grönland';
case GM = 'Gambia';
case GN = 'Guinea';
case GP = 'Guadeloupe';
case GQ = 'Äquatorialguinea';
case GR = 'Griechenland';
case GS = 'Südgeorgien und Südliche Sandwich-Inseln';
case GT = 'Guatemala';
case GU = 'Guam';
case GW = 'Guinea-Bissau';
case GY = 'Guyana';
case HK = 'Kong Hong';
case HM = 'Heard und Mc Donald Islands';
case HN = 'Honduras';
case HT = 'Haiti';
case HU = 'Ungarn';
case ID = 'Indonesien';
case IE = 'Irland';
case IL = 'Israel';
case IN = 'Indien';
case IO = 'British Indian Ocean Territory';
case IQ = 'Irak';
case IR = 'Iran (Islamische Republik)';
case IS = 'Island';
case IT = 'Italien';
case JM = 'Jamaica';
case JO = 'Jordanien';
case JP = 'Japan';
case KE = 'Kenya';
case KG = 'Kirgisien';
case KH = 'Königreich Kambodscha';
case KI = 'Kiribati';
case KM = 'Komoren';
case KN = 'Saint Kitts und Nevis';
case KP = 'Korea, Volksrepublik';
case KR = 'Korea';
case KW = 'Kuwait';
case KY = 'Kayman Islands';
case KZ = 'Kasachstan';
case LA = 'Laos';
case LB = 'Libanon';
case LC = 'Saint Lucia';
case LI = 'Liechtenstein';
case LK = 'Sri Lanka';
case LR = 'Liberia';
case LS = 'Lesotho';
case LT = 'Littauen';
case LU = 'Luxemburg';
case LV = 'Lettland';
case LY = 'Libyen';
case MA = 'Marokko';
case MC = 'Monaco';
case MD = 'Moldavien';
case MG = 'Madagaskar';
case MH = 'Marshall-Inseln';
case MK = 'Mazedonien, ehem. Jugoslawische Republik';
case ML = 'Mali';
case MM = 'Myanmar';
case MN = 'Mongolei';
case MO = 'Macao';
case MP = 'Nördliche Marianneninseln';
case MQ = 'Martinique';
case MR = 'Mauretanien';
case MS = 'Montserrat';
case MT = 'Malta';
case MU = 'Mauritius';
case MV = 'Malediven';
case MW = 'Malawi';
case MX = 'Mexico';
case MY = 'Malaysien';
case MZ = 'Mozambique';
case NA = 'Namibia';
case NC = 'Neu Kaledonien';
case NE = 'Niger';
case NF = 'Norfolk Island';
case NG = 'Nigeria';
case NI = 'Nicaragua';
case NL = 'Niederlande';
case NO = 'Norwegen';
case NP = 'Nepal';
case NR = 'Nauru';
case NU = 'Niue';
case NZ = 'Neuseeland';
case OM = 'Oman';
case PA = 'Panama';
case PE = 'Peru';
case PF = 'Französisch Polynesien';
case PG = 'Papua Neuguinea';
case PH = 'Philippinen';
case PK = 'Pakistan';
case PL = 'Polen';
case PM = 'St. Pierre und Miquelon';
case PN = 'Pitcairn';
case PR = 'Puerto Rico';
case PT = 'Portugal';
case PW = 'Palau';
case PY = 'Paraguay';
case QA = 'Katar';
case RE = 'Reunion';
case RO = 'Rumänien';
case RU = 'Russische Föderation';
case RW = 'Ruanda';
case SA = 'Saudi Arabien';
case SB = 'Salomonen';
case SC = 'Seychellen';
case SD = 'Sudan';
case SE = 'Schweden';
case SG = 'Singapur';
case SH = 'St. Helena';
case SI = 'Slovenien';
case SJ = 'Svalbard und Jan Mayen Islands';
case SK = 'Slowakei';
case SL = 'Sierra Leone';
case SM = 'San Marino';
case SN = 'Senegal';
case SO = 'Somalia';
case SR = 'Surinam';
case ST = 'Sao Tome und Principe';
case SV = 'El Salvador';
case SY = 'Syrien, Arabische Republik';
case SZ = 'Swaziland';
case TC = 'Turk und Caicos-Inseln';
case TD = 'Tschad';
case TF = 'Französisches Südl.Territorium';
case TG = 'Togo';
case TH = 'Thailand';
case TJ = 'Tadschikistan';
case TK = 'Tokelau';
case TM = 'Turkmenistan';
case TN = 'Tunesien';
case TO = 'Tonga';
case TP = 'Ost-Timor';
case TR = 'Türkei';
case TT = 'Trinidad und Tobago';
case TV = 'Tuvalu';
case TW = 'Taiwan';
case TZ = 'Tansania, United Republic of';
case UA = 'Ukraine';
case UG = 'Uganda';
case GB = 'Großbritannien';
case US = 'Vereinigte Staaten';
case UM = 'Vereinigte Staaten, Minor Outlying Islands';
case UY = 'Uruguay';
case UZ = 'Usbekistan';
case VA = 'Vatikanstaat';
case VC = 'Saint Vincent und Grenadines';
case VE = 'Venezuela';
case VG = 'Virgin Islands (Britisch)';
case VI = 'Virgin Islands (U.S.)';
case VN = 'Vietnam';
case VU = 'Vanuatu';
case WF = 'Wallis und Futuna Islands';
case WS = 'Samoa';
case YE = 'Jemen';
case YT = 'Mayotte';
case YU = 'Jugoslawien';
case ZA = 'Südafrika';
case ZM = 'Sambia';
case ZW = 'Zimbabw';
/**
* @return array<int, array{name: string, id: string}>
*/
public static function forSelect(): array
{
return collect(static::cases())
->map(fn ($case) => ['id' => $case->value, 'name' => $case->value])
->toArray();
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Contribution\Requests;
use App\Contribution\Data\MemberData;
use Illuminate\Support\Collection;
class GenerateApiRequest extends GenerateRequest {
/**
* @return array<string, string>
*/
public function payload(): array
{
return $this->input();
}
public function validateContribution(): void {
}
public function members(): Collection {
return MemberData::fromApi($this->value('members'));
}
}

View File

@ -1,68 +0,0 @@
<?php
namespace App\Contribution\Requests;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\ContributionFactory;
use App\Contribution\Data\MemberData;
use App\Contribution\Documents\ContributionDocument;
use App\Country;
use Lorisleiva\Actions\ActionRequest;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
class GenerateRequest extends ActionRequest implements HasContributionData {
/**
* @return array<string, string>
*/
protected function payload(): array
{
return json_decode(rawurldecode(base64_decode($this->input('payload', ''))), true);
}
public function validateContribution(): void {
Validator::make($this->payload(), app(ContributionFactory::class)->rules($this->type()))->validate();
}
/**
* @return string|array<array-key, mixed>
*/
public function value(string $key): string|array
{
return data_get($this->payload(), $key);
}
/**
* @return class-string<ContributionDocument>
*/
public function type(): string
{
return $this->value('type');
}
public function dateFrom(): Carbon {
return Carbon::parse($this->value('dateFrom'));
}
public function dateUntil(): Carbon {
return Carbon::parse($this->value('dateUntil'));
}
public function zipLocation(): string {
return $this->value('zipLocation');
}
public function eventName(): string {
return $this->value('eventName');
}
public function members(): Collection {
return MemberData::fromModels($this->value('members'));
}
public function country(): ?Country {
return Country::where('id', $this->value('country'))->first();
}
}

View File

@ -31,27 +31,25 @@ class CreateExcelDocumentAction
private function allSheet(Collection $participants): TableDocumentData
{
$document = TableDocumentData::from(['title' => 'Anmeldungen für ' . $this->form->name, 'sheets' => []]);
$headers = $this->form->getFields()->names()->push('Abgemeldet am')->prepend('ID')->toArray();
[$activeParticipants, $cancelledParticipants] = $participants->partition(fn ($participant) => $participant->cancelled_at === null);
$headers = $this->form->getFields()->map(fn ($field) => $field->name)->toArray();
$document->addSheet(SheetData::from([
'header' => $headers,
'data' => $this->rowsFor($activeParticipants),
'data' => $participants
->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray())
->toArray(),
'name' => 'Alle',
]));
$document->addSheet(SheetData::from([
'header' => $headers,
'data' => $this->rowsFor($cancelledParticipants),
'name' => 'Abgemeldet',
]));
if ($this->form->export->groupBy) {
$groups = $activeParticipants->groupBy(fn ($participant) => $participant->getFields()->findByKey($this->form->export->groupBy)->presentRaw());
$groups = $participants->groupBy(fn ($participant) => $participant->getFields()->findByKey($this->form->export->groupBy)->presentRaw());
foreach ($groups as $name => $groupedParticipants) {
foreach ($groups as $name => $participants) {
$document->addSheet(SheetData::from([
'header' => $headers,
'data' => $this->rowsFor($groupedParticipants),
'data' => $participants
->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray())
->toArray(),
'name' => $name,
]));
}
@ -66,17 +64,6 @@ class CreateExcelDocumentAction
return $document;
}
/**
* @param Collection<int, Participant> $participants
* @return array<int, array<string, mixed>>
*/
public function rowsFor(Collection $participants): array {
return $participants->map(fn ($participant) => $participant->getFields()->presentValues()
->put('Abgemeldet am', $participant->cancelled_at?->format('d.m.Y H:i:s') ?: '')
->prepend((string) $participant->id, 'ID')
)->toArray();
}
private function tempPath(): string
{
return sys_get_temp_dir() . '/' . str()->uuid()->toString();

View File

@ -16,7 +16,7 @@ class FormApiListAction
/**
* @param string $filter
* @return LengthAwarePaginator<int, Form>
* @return LengthAwarePaginator<Form>
*/
public function handle(string $filter, int $perPage): LengthAwarePaginator
{

View File

@ -1,38 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class FormCopyAction
{
use AsAction;
public function handle(Form $form): Form
{
$newForm = $form->replicate();
$newForm->save();
$newForm->update(['name' => $form->name.' - Kopie', 'is_active' => false]);
foreach ($form->getRegisteredMediaCollections() as $collection) {
foreach ($form->getMedia($collection->name) as $media) {
$media->copy($newForm, $collection->name);
}
}
ClearFrontendCacheAction::run();
return $form;
}
public function asController(Form $form): JsonResponse
{
$this->handle($form);
Succeeded::message('Veranstaltung kopiert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\FormSettings;
use App\Form\Models\Form;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\URL;
use Lorisleiva\Actions\Concerns\AsAction;
class FormGenerateLaterlinkAction
{
use AsAction;
public function asController(Form $form): JsonResponse
{
$registerUrl = str(app(FormSettings::class)->registerUrl)->replace('{slug}', $form->slug)->toString();
$laterId = str()->uuid()->toString();
$laterUrl = URL::signedRoute('form.register', ['form' => $form, 'later' => '1', 'id' => $laterId]);
$urlParts = parse_url($laterUrl);
Cache::remember('later_'.$laterId, 2592000, fn () => $form->id); // Link ist 40 Tage gültig
return response()->json([
'url' => $registerUrl.'?'.data_get($urlParts, 'query')
]);
}
}

View File

@ -16,11 +16,11 @@ class FormIndexAction
use AsAction;
/**
* @return LengthAwarePaginator<int, Form>
* @return LengthAwarePaginator<Form>
*/
public function handle(string $filter): LengthAwarePaginator
{
return FormFilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount(['participants' => fn ($q) => $q->whereNull('cancelled_at')]))->paginate(15);
return FormFilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount('participants'))->paginate(15);
}
public function asController(ActionRequest $request): Response

View File

@ -36,10 +36,6 @@ class FormStoreAction
'needs_prevention' => 'present|boolean',
'prevention_text' => 'array',
'prevention_conditions' => 'array',
'leader_conditions' => 'array',
'zip' => 'present|nullable|string',
'location' => 'present|nullable|string',
'country' => 'nullable|string|max:255',
];
}

View File

@ -3,6 +3,7 @@
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Editor\Condition;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
@ -35,10 +36,6 @@ class FormUpdateAction
'needs_prevention' => 'present|boolean',
'prevention_text' => 'array',
'prevention_conditions' => 'array',
'location' => 'present|nullable|string',
'zip' => 'present|nullable|string',
'country' => 'nullable|string|max:255',
'leader_conditions' => 'array',
];
}

View File

@ -14,7 +14,7 @@ class FormtemplateIndexAction
use AsAction;
/**
* @return LengthAwarePaginator<int, Formtemplate>
* @return LengthAwarePaginator<Formtemplate>
*/
public function handle(): LengthAwarePaginator
{

View File

@ -1,45 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\ContributionFactory;
use App\Form\Models\Form;
use App\Form\Requests\FormCompileRequest;
use App\Rules\JsonBase64Rule;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
class GenerateContributionAction
{
use AsAction;
public function handle(HasContributionData $request): BaseCompiler
{
return Tex::compile($request->type()::fromPayload($request));
}
public function asController(ActionRequest $request, Form $form): BaseCompiler|JsonResponse
{
$r = FormCompileRequest::from(['form' => $form]);
app(ContributionFactory::class)->validateType($r);
$r->validateContribution();
return $request->input('validate')
? response()->json([])
: $this->handle($r);
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'payload' => [new JsonBase64Rule()],
];
}
}

View File

@ -6,7 +6,6 @@ use App\Form\Models\Participant;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantDestroyAction
@ -14,20 +13,14 @@ class ParticipantDestroyAction
use AsAction;
use TracksJob;
public function handle(int $participantId, bool $force): void
public function handle(int $participantId): void
{
$participant = Participant::findOrFail($participantId);
if ($force) {
$participant->delete();
} else {
$participant->update(['cancelled_at' => now()]);
}
Participant::findOrFail($participantId)->delete();
}
public function asController(ActionRequest $request, Participant $participant): void
public function asController(Participant $participant): void
{
$this->startJob($participant->id, $request->header('X-Force') === '1');
$this->startJob($participant->id);
}
/**

View File

@ -7,9 +7,6 @@ use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Member\Member;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
@ -23,9 +20,13 @@ 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());
->reduce(fn ($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery());
$member = $form->getFields()->withNamiType()->count() && $memberQuery->count() === 1 ? $memberQuery->first() : null;
$participant = $form->participants()->create([
@ -33,7 +34,7 @@ class RegisterAction
'member_id' => $member?->id,
]);
$form->getFields()->each(fn($field) => $field->afterRegistration($form, $participant, $input));
$form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input));
$participant->sendConfirmationMail();
ExportSyncAction::dispatch($form->id);
@ -76,30 +77,8 @@ class RegisterAction
public function asController(ActionRequest $request, Form $form): JsonResponse
{
if (!$form->canRegister() && !$this->isRegisteringLater($request, $form)) {
throw ValidationException::withMessages(['event' => 'Anmeldung zzt nicht möglich.']);
}
$participant = $this->handle($form, $request->validated());
if ($this->isRegisteringLater($request, $form)) {
Cache::forget('later_'.request('id'));
}
return response()->json($participant);
}
public function isRegisteringLater(ActionRequest $request, Form $form): bool {
$validator = Validator::make($request->query(), [
'later' => 'required|numeric|in:1',
'id' => 'required|string|uuid:4',
'signature' => 'required|string',
]);
if (!URL::hasValidSignature($request) || $validator->fails()) {
return false;
}
return Cache::get('later_'.data_get($validator->validated(), 'id')) === $form->id;
}
}

View File

@ -18,10 +18,10 @@ class UpdateParticipantSearchIndexAction
$form->searchableUsing()->updateIndexSettings(
$form->participantsSearchableAs(),
[
'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id', 'cancelled_at'],
'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id'],
'searchableAttributes' => $form->getFields()->searchables()->getKeys(),
'sortableAttributes' => [...$form->getFields()->sortables()->getKeys(), 'id', 'created_at'],
'displayedAttributes' => [...$form->getFields()->filterables()->getKeys(), ...$form->getFields()->searchables()->getKeys(), 'id', 'cancelled_at'],
'displayedAttributes' => [...$form->getFields()->filterables()->getKeys(), ...$form->getFields()->searchables()->getKeys(), 'id'],
'pagination' => [
'maxTotalHits' => 1000000,
]

View File

@ -99,27 +99,22 @@ class FieldCollection extends Collection
}
/**
* @return Collection<int, string>
* @return array<int, string>
*/
public function names(): Collection
public function names(): array
{
return $this->map(fn ($field) => $field->name);
return $this->map(fn ($field) => $field->name)->toArray();
}
/**
* @return Collection<string, string>
* @return array<int, string>
*/
public function presentValues(): Collection
public function presentValues(): array
{
return $this->mapWithKeys(fn ($field) => [$field->name => $field->presentRaw()]);
return $this->map(fn ($field) => $field->presentRaw())->toArray();
}
public function hasSpecialType(SpecialType $specialType): bool
{
return $this->findBySpecialType($specialType) !== null;
}
public function findBySpecialType(SpecialType $specialType): ?Field
private function findBySpecialType(SpecialType $specialType): ?Field
{
return $this->first(fn ($field) => $field->specialType === $specialType);
}

View File

@ -7,11 +7,6 @@ enum SpecialType: string
case FIRSTNAME = 'Vorname';
case LASTNAME = 'Nachname';
case EMAIL = 'E-Mail-Adresse';
case BIRTHDAY = 'Geburtsdatum';
case ZIP = 'PLZ';
case LOCATION = 'Ort';
case ADDRESS = 'Adresse';
case GENDER = 'Geschlecht';
/**
* @return array<int, array{name: string, id: string}>

View File

@ -2,6 +2,7 @@
namespace App\Form;
use App\Form\Actions\SettingStoreAction;
use App\Setting\Contracts\Storeable;
use App\Setting\LocalSettings;
use Lorisleiva\Actions\ActionRequest;
@ -10,7 +11,6 @@ class FormSettings extends LocalSettings implements Storeable
{
public string $registerUrl;
public string $clearCacheUrl;
public ?string $replyToMail;
public static function group(): string
{
@ -19,7 +19,7 @@ class FormSettings extends LocalSettings implements Storeable
public static function title(): string
{
return 'Veranstaltungen';
return 'Formulare';
}
/**
@ -30,7 +30,6 @@ class FormSettings extends LocalSettings implements Storeable
return [
'registerUrl' => 'present|string',
'clearCacheUrl' => 'present|string',
'replyToMail' => 'nullable|string|email',
];
}
@ -48,7 +47,6 @@ class FormSettings extends LocalSettings implements Storeable
'data' => [
'registerUrl' => $this->registerUrl,
'clearCacheUrl' => $this->clearCacheUrl,
'replyToMail' => $this->replyToMail,
]
]
];

View File

@ -4,7 +4,6 @@ namespace App\Form\Mails;
use App\Form\Data\FormConfigData;
use App\Form\Editor\FormConditionResolver;
use App\Form\FormSettings;
use App\Form\Models\Participant;
use App\Lib\Editor\Condition;
use Illuminate\Bus\Queueable;
@ -25,8 +24,6 @@ class ConfirmRegistrationMail extends Mailable
/** @var array<int, mixed> */
public array $bottomText;
public FormSettings $formSettings;
/**
* Create a new message instance.
*
@ -35,7 +32,6 @@ class ConfirmRegistrationMail extends Mailable
public function __construct(public Participant $participant)
{
$conditionResolver = app(FormConditionResolver::class)->forParticipant($participant);
$this->formSettings = app(FormSettings::class);
$this->fullname = $participant->getFields()->getFullname();
$this->config = $participant->getConfig();
$this->topText = $conditionResolver->makeBlocks($participant->form->mail_top);
@ -49,15 +45,9 @@ class ConfirmRegistrationMail extends Mailable
*/
public function envelope()
{
$envelope = new Envelope(
return new Envelope(
subject: 'Deine Anmeldung zu ' . $this->participant->form->name,
);
if ($this->formSettings->replyToMail !== null) {
$envelope->replyTo($this->formSettings->replyToMail);
}
return $envelope;
}
/**
@ -79,8 +69,10 @@ class ConfirmRegistrationMail extends Mailable
*/
public function attachments()
{
$conditionResolver = app(FormConditionResolver::class)->forParticipant($this->participant);
return $this->participant->form->getMedia('mailattachments')
->filter(fn ($media) => $this->participant->matchesCondition(Condition::fromMedia($media)))
->filter(fn ($media) => $conditionResolver->filterCondition(Condition::fromMedia($media)))
->map(fn ($media) => Attachment::fromStorageDisk($media->disk, $media->getPathRelativeToRoot()))
->all();
}

View File

@ -2,7 +2,6 @@
namespace App\Form\Models;
use App\Contribution\Enums\Country;
use App\Form\Actions\UpdateParticipantSearchIndexAction;
use App\Form\Data\ExportData;
use App\Form\Data\FieldCollection;
@ -19,6 +18,7 @@ use Laravel\Scout\Searchable;
use Spatie\Image\Enums\Fit;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Zoomyboy\MedialibraryHelper\DefersUploads;
/** @todo replace editor content with EditorData cast */
@ -49,8 +49,6 @@ class Form extends Model implements HasMedia
'to' => 'datetime',
'registration_from' => 'datetime',
'registration_until' => 'datetime',
'country' => Country::class,
'leader_conditions' => Condition::class,
];
/**
@ -71,24 +69,25 @@ class Form extends Model implements HasMedia
return $this->hasMany(Participant::class);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('headerImage')
->singleFile()
->maxWidth(fn() => 500)
->forceFileName(fn(Form $model) => $model->slug)
->convert(fn() => 'jpg')
->registerMediaConversions(function () {
->maxWidth(fn () => 500)
->forceFileName(fn (Form $model, string $name) => $model->slug)
->convert(fn () => 'jpg')
->registerMediaConversions(function (Media $media) {
$this->addMediaConversion('square')->fit(Fit::Crop, 400, 400);
});
$this->addMediaCollection('mailattachments')
->withDefaultProperties(fn() => [
->withDefaultProperties(fn () => [
'conditions' => [
'mode' => 'all',
'ifs' => []
],
])
->withPropertyValidation(fn() => [
->withPropertyValidation(fn () => [
'conditions.mode' => 'required|string|in:all,any',
'conditions.ifs' => 'array',
'conditions.ifs.*.field' => 'required',
@ -102,7 +101,7 @@ class Form extends Model implements HasMedia
*/
public function getRegistrationRules(): array
{
return $this->getFields()->reduce(fn($carry, $field) => [
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationRules($this),
], []);
@ -113,7 +112,7 @@ class Form extends Model implements HasMedia
*/
public function getRegistrationMessages(): array
{
return $this->getFields()->reduce(fn($carry, $field) => [
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationMessages($this),
], []);
@ -124,7 +123,7 @@ class Form extends Model implements HasMedia
*/
public function getRegistrationAttributes(): array
{
return $this->getFields()->reduce(fn($carry, $field) => [
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationAttributes($this),
], []);
@ -190,7 +189,8 @@ class Form extends Model implements HasMedia
return Sorting::from($this->meta['sorting']);
}
public function isInDates(): bool {
public function canRegister(): bool
{
if ($this->registration_from && $this->registration_from->gt(now())) {
return false;
}
@ -201,9 +201,4 @@ class Form extends Model implements HasMedia
return true;
}
public function canRegister(): bool
{
return $this->is_active && $this->isInDates();
}
}

View File

@ -4,12 +4,12 @@ namespace App\Form\Models;
use App\Form\Data\FieldCollection;
use App\Form\Data\FormConfigData;
use App\Form\Editor\FormConditionResolver;
use App\Form\Mails\ConfirmRegistrationMail;
use App\Lib\Editor\Condition;
use App\Form\Scopes\ParticipantFilterScope;
use App\Member\Member;
use App\Prevention\Contracts\Preventable;
use Database\Factories\Form\Models\ParticipantFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -31,7 +31,6 @@ class Participant extends Model implements Preventable
public $casts = [
'data' => 'json',
'last_remembered_at' => 'datetime',
'cancelled_at' => 'datetime',
];
/**
@ -109,15 +108,6 @@ class Participant extends Model implements Preventable
/** @return array<string, mixed> */
public function toSearchableArray(): array
{
return [
...$this->data,
'parent-id' => $this->parent_id,
'created_at' => $this->created_at->timestamp,
'cancelled_at' => $this->cancelled_at
];
}
public function matchesCondition(Condition $condition): bool {
return app(FormConditionResolver::class)->forParticipant($this)->filterCondition($condition);
return [...$this->data, 'parent-id' => $this->parent_id, 'created_at' => $this->created_at->timestamp];
}
}

View File

@ -1,107 +0,0 @@
<?php
namespace App\Form\Requests;
use App\Contribution\Contracts\HasContributionData;
use App\Contribution\Data\MemberData;
use App\Contribution\Documents\ContributionDocument;
use App\Country;
use App\Form\Editor\FormConditionResolver;
use App\Form\Enums\SpecialType;
use App\Form\Models\Form;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Spatie\LaravelData\Data;
class FormCompileRequest extends Data implements HasContributionData {
public function __construct(public Form $form) {}
/**
* @return class-string<ContributionDocument>
*/
public function type(): string
{
$payload = json_decode(rawurldecode(base64_decode(request()->input('payload'))), true);
return $payload['type'];
}
public function dateFrom(): Carbon
{
return $this->form->from;
}
public function dateUntil(): Carbon
{
return $this->form->to;
}
public function zipLocation(): string
{
return $this->form->zip.' '.$this->form->location;
}
public function eventName(): string
{
return $this->form->name;
}
public function members(): Collection
{
$members = [];
$fields = [
[SpecialType::FIRSTNAME, 'firstname'],
[SpecialType::LASTNAME, 'lastname'],
[SpecialType::BIRTHDAY, 'birthday'],
[SpecialType::GENDER, 'gender'],
[SpecialType::ADDRESS, 'address'],
[SpecialType::ZIP, 'zip'],
[SpecialType::LOCATION, 'location'],
];
foreach ($this->form->participants as $participant) {
$member = [];
foreach ($fields as [$type, $name]) {
$f = $this->form->getFields()->findBySpecialType($type);
if (!$f) {
continue;
}
$member[$name] = $participant->getFields()->find($f)->value;
}
$members[] = [
'is_leader' => $participant->matchesCondition($participant->form->leader_conditions),
'gender' => 'weiblich',
...$member,
];
}
return MemberData::fromApi($members);
}
public function country(): ?Country
{
return Country::first();
}
public function validateContribution(): void
{
Validator::make($this->form->toArray(), [
'zip' => 'required',
'location' => 'required'
])
->after(function($validator) {
foreach ($this->type()::requiredFormSpecialTypes() as $type) {
if (!$this->form->getFields()->hasSpecialType($type)) {
$validator->errors()->add($type->name, 'Kein Feld für ' . $type->value . ' vorhanden.');
}
}
if ($this->form->participants->count() === 0) {
$validator->errors()->add('participants', 'Veranstaltung besitzt noch keine Teilnehmer*innen.');
}
})
->validate();
}
}

View File

@ -2,7 +2,6 @@
namespace App\Form\Resources;
use App\Contribution\Enums\Country;
use App\Form\Data\ExportData;
use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType;
@ -15,7 +14,6 @@ use App\Group;
use App\Lib\Editor\EditorData;
use App\Lib\HasMeta;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Contribution\ContributionFactory;
/**
* @mixin Form
@ -46,7 +44,6 @@ class FormResource extends JsonResource
'mail_bottom' => $this->mail_bottom,
'registration_from' => $this->registration_from?->format('Y-m-d H:i:s'),
'registration_until' => $this->registration_until?->format('Y-m-d H:i:s'),
'is_in_dates' => $this->isInDates(),
'config' => $this->config,
'participants_count' => $this->participants_count,
'is_active' => $this->is_active,
@ -56,21 +53,14 @@ class FormResource extends JsonResource
'needs_prevention' => $this->needs_prevention,
'prevention_text' => $this->prevention_text,
'prevention_conditions' => $this->prevention_conditions,
'leader_conditions' => $this->leader_conditions,
'zip' => $this->zip,
'location' => $this->location,
'country' => $this->country,
'links' => [
'participant_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => null]),
'participant_root_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => -1]),
'update' => route('form.update', $this->getModel()),
'destroy' => route('form.destroy', $this->getModel()),
'is_dirty' => route('form.is-dirty', $this->getModel()),
'update' => route('form.update', ['form' => $this->getModel()]),
'destroy' => route('form.destroy', ['form' => $this->getModel()]),
'is_dirty' => route('form.is-dirty', ['form' => $this->getModel()]),
'frontend' => str(app(FormSettings::class)->registerUrl)->replace('{slug}', $this->slug),
'export' => route('form.export', $this->getModel()),
'copy' => route('form.copy', $this->getModel()),
'contribution' => route('form.contribution', $this->getModel()),
'laterlink' => route('form.laterlink', $this->getModel()),
'export' => route('form.export', ['form' => $this->getModel()]),
]
];
}
@ -92,8 +82,6 @@ class FormResource extends JsonResource
'templates' => FormtemplateResource::collection(Formtemplate::get()),
'namiTypes' => NamiType::forSelect(),
'specialTypes' => SpecialType::forSelect(),
'countries' => Country::forSelect(),
'contribution_types' => app(ContributionFactory::class)->compilerSelect(),
'default' => [
'description' => [],
'is_active' => true,
@ -113,9 +101,6 @@ class FormResource extends JsonResource
'id' => null,
'export' => ExportData::from([]),
'prevention_conditions' => ['mode' => 'all', 'ifs' => []],
'zip' => '',
'location' => '',
'country' => null,
],
'section_default' => [
'name' => '',

View File

@ -32,8 +32,7 @@ class ParticipantFilterScope extends ScoutFilter
public string $search = '',
public array $options = [],
public ?int $parent = null,
public ?Sorting $sort = null,
public bool $showCancelled = false,
public ?Sorting $sort = null
) {
}
@ -55,12 +54,6 @@ class ParticipantFilterScope extends ScoutFilter
$filter->push('parent-id IS NULL');
}
if ($this->showCancelled) {
$filter->push('cancelled_at IS NOT NULL');
} else {
$filter->push('cancelled_at IS NULL');
}
if ($this->parent !== null && $this->parent !== -1) {
$filter->push('parent-id = ' . $this->parent);
}

View File

@ -71,7 +71,7 @@ class InitializeAction
public function asController(ActionRequest $request, NamiSettings $settings): RedirectResponse
{
$settings->mglnr = $request->input('mglnr');
$settings->mglnr = (int) $request->input('mglnr');
$settings->password = $request->input('password');
$settings->default_group_id = (int) $request->input('group_id');
$settings->search_params = $request->input('params');

View File

@ -21,7 +21,7 @@ class NamiGetSearchLayerAction
*/
public function handle(array $input): Collection
{
return Nami::login($input['mglnr'], $input['password'])->searchLayerOptions(
return Nami::login((int) $input['mglnr'], $input['password'])->searchLayerOptions(
SearchLayer::from($input['layer'] ?: 0),
$input['parent'] ?: null
);

View File

@ -16,7 +16,7 @@ class NamiLoginCheckAction
*/
public function handle(array $input): void
{
Nami::freshLogin($input['mglnr'], $input['password']);
Nami::freshLogin((int) $input['mglnr'], $input['password']);
}
/**

View File

@ -16,7 +16,7 @@ class NamiSearchAction
/**
* @param array<string, mixed> $params
*
* @return LengthAwarePaginator<int, MemberEntry>
* @return LengthAwarePaginator<MemberEntry>
*/
public function handle(Api $api, int $page, array $params, int $perPage = 10): LengthAwarePaginator
{
@ -36,7 +36,7 @@ class NamiSearchAction
}
/**
* @return LengthAwarePaginator<int, MemberEntry>
* @return LengthAwarePaginator<MemberEntry>
*/
public function asController(ActionRequest $request): LengthAwarePaginator
{

View File

@ -1,22 +0,0 @@
<?php
namespace App\Lib;
use Spatie\LaravelData\PaginatedDataCollection;
/**
* @mixin Spatie\LaravelData\Data
*/
trait HasDataMeta
{
/**
* @return array<string, mixed>
*/
public static function collectPages(mixed $items): array {
$source = parent::collect($items, PaginatedDataCollection::class)->toArray();
return [
...parent::collect($items, PaginatedDataCollection::class)->toArray(),
'meta' => [...$source['meta'], ...static::meta()]
];
}
}

View File

@ -2,9 +2,9 @@
namespace App\Lib;
/**
* @mixin \Illuminate\Http\Resources\Json\JsonResource
*/
use Spatie\LaravelData\PaginatedDataCollection;
/** @mixin \Illuminate\Http\Resources\Json\JsonResource */
trait HasMeta
{
/**
@ -43,4 +43,12 @@ trait HasMeta
{
return [];
}
public static function collectPages(mixed $items): array {
$source = parent::collect($items, PaginatedDataCollection::class)->toArray();
return [
...parent::collect($items, PaginatedDataCollection::class)->toArray(),
'meta' => [...$source['meta'], ...static::meta()]
];
}
}

View File

@ -7,7 +7,7 @@ use App\Group;
use Spatie\LaravelData\Data;
use App\Lib\Data\DateData;
use App\Lib\Data\RecordData;
use App\Lib\HasDataMeta;
use App\Lib\HasMeta;
use App\Member\Membership;
use App\Membership\FilterScope;
use App\Subactivity;
@ -15,11 +15,8 @@ use App\Subactivity;
class MembershipData extends Data
{
use HasDataMeta;
use HasMeta;
/**
* @param array<string, string> $links
*/
public function __construct(
public int $id,
public RecordData $activity,
@ -52,9 +49,6 @@ class MembershipData extends Data
]);
}
/**
* @return array<string, mixed>
*/
public static function meta(): array {
return [
'activities' => RecordData::collect(Activity::get()),

View File

@ -75,7 +75,6 @@ class MemberRequest extends FormRequest
'send_newspaper' => 'boolean',
'main_phone' => ['nullable', new ValidPhoneRule('Telefon (Eltern)')],
'mobile_phone' => ['nullable', new ValidPhoneRule('Handy (Eltern)')],
'work_phone' => ['nullable', new ValidPhoneRule('Tel geschäftlich')],
'invoice_address' => '',
'gender_id' => 'nullable|exists:genders,id',
'region_id' => 'nullable|exists:regions,id',

View File

@ -21,7 +21,7 @@ class MassStoreAction
use TracksJob;
/**
* @return array<string, mixed>
* @return array<string, string>
*/
public function rules(): array
{
@ -54,7 +54,9 @@ class MassStoreAction
Membership::where($attributes)->active()->whereNotIn('member_id', $members)->get()
->each(fn ($membership) => MembershipDestroyAction::run($membership->id));
Member::whereIn('id', $members)->whereDoesntHave('memberships', fn ($q) => $q->where($attributes))->get()
collect($members)
->except(Membership::where($attributes)->active()->pluck('member_id'))
->map(fn ($memberId) => Member::findOrFail($memberId))
->each(fn ($member) => MembershipStoreAction::run(
$member,
$activity,
@ -63,6 +65,7 @@ class MassStoreAction
null,
));
ResyncAction::dispatch();
});
}
@ -84,9 +87,6 @@ class MassStoreAction
return response()->json([], 200);
}
/**
* @return array<string, string>
*/
public function getValidationAttributes(): array {
return [
'activity_id' => 'Tätigkeit',

View File

@ -25,7 +25,7 @@ class FilterScope extends Filter
public function getQuery(): Builder
{
$query = Membership::orderByRaw('member_id, activity_id, subactivity_id');
$query = (new Membership())->newQuery();
if ($this->active === true) {
$query = $query->active();

View File

@ -24,7 +24,6 @@ class SettingStoreAction
'weeks' => 'required|numeric|gte:0',
'freshRememberInterval' => 'required|numeric|gte:0',
'active' => 'boolean',
'replyToMail' => 'nullable|string|email',
];
}
@ -34,7 +33,6 @@ class SettingStoreAction
$settings->formmail = EditorData::from($request->formmail);
$settings->yearlymail = EditorData::from($request->yearlymail);
$settings->weeks = $request->weeks;
$settings->replyToMail = $request->replyToMail;
$settings->freshRememberInterval = $request->freshRememberInterval;
$settings->active = $request->active;
$settings->yearlyMemberFilter = FilterScope::from($request->yearlyMemberFilter);

View File

@ -6,7 +6,6 @@ use App\Invoice\InvoiceSettings;
use App\Lib\Editor\EditorData;
use App\Prevention\Contracts\Preventable;
use App\Prevention\Data\PreventionData;
use App\Prevention\PreventionSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment;
use Illuminate\Mail\Mailable;
@ -20,7 +19,6 @@ class YearlyMail extends Mailable
use Queueable, SerializesModels;
public InvoiceSettings $settings;
public PreventionSettings $preventionSettings;
/**
* Create a new message instance.
@ -29,7 +27,6 @@ class YearlyMail extends Mailable
public function __construct(public Preventable $preventable, public EditorData $bodyText, public Collection $preventions)
{
$this->settings = app(InvoiceSettings::class);
$this->preventionSettings = app(PreventionSettings::class);
$this->bodyText = $this->bodyText
->replaceWithList('wanted', $preventions->map(fn($prevention) => $prevention->text())->toArray());
}
@ -41,15 +38,9 @@ class YearlyMail extends Mailable
*/
public function envelope()
{
$envelope = (new Envelope(
return (new Envelope(
subject: $this->preventable->preventableSubject(),
))->to($this->preventable->getMailRecipient()->email, $this->preventable->getMailRecipient()->name);
if ($this->preventionSettings->replyToMail !== null) {
$envelope->replyTo($this->preventionSettings->replyToMail);
}
return $envelope;
}
/**

View File

@ -15,7 +15,6 @@ class PreventionSettings extends LocalSettings
public int $freshRememberInterval;
public bool $active;
public FilterScope $yearlyMemberFilter;
public ?string $replyToMail;
/**
* @var array<int, string>
* @todo Create collection cast to Collection of enums
@ -50,7 +49,6 @@ class PreventionSettings extends LocalSettings
...$this->toArray(),
'weeks' => (string) $this->weeks,
'freshRememberInterval' => (string) $this->freshRememberInterval,
'replyToMail' => $this->replyToMail,
];
}
}

View File

@ -14,7 +14,7 @@ class SearchAction
use AsAction;
/**
* @return LengthAwarePaginator<int, array<string, mixed>>
* @return LengthAwarePaginator<array<string, mixed>>
*/
public function handle(ActionRequest $request): LengthAwarePaginator
{

View File

@ -4,6 +4,7 @@ namespace App\Setting;
use App\Group;
use App\Initialize\Actions\NamiLoginCheckAction;
use App\Nami\Actions\SettingSaveAction;
use App\Setting\Contracts\Storeable;
use Lorisleiva\Actions\ActionRequest;
use Zoomyboy\LaravelNami\Api;
@ -11,7 +12,7 @@ use Zoomyboy\LaravelNami\Nami;
class NamiSettings extends LocalSettings implements Storeable
{
public string $mglnr;
public int $mglnr;
public string $password;

View File

@ -47,8 +47,8 @@ class FormFactory extends Factory
'description' => EditorRequestFactory::new()->toData(),
'excerpt' => $this->faker->words(10, true),
'config' => ['sections' => []],
'from' => $this->faker->dateTimeBetween('+1 week', '+3 weeks')->format('Y-m-d'),
'to' => $this->faker->dateTimeBetween('+4 week', '+6 weeks')->format('Y-m-d'),
'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(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(),
@ -57,8 +57,6 @@ class FormFactory extends Factory
'is_private' => false,
'export' => ExportData::from([]),
'prevention_conditions' => Condition::defaults(),
'zip' => $this->faker->numberBetween(1100, 99999),
'location' => $this->faker->city(),
];
}
@ -67,7 +65,7 @@ class FormFactory extends Factory
*/
public function sections(array $sections): self
{
return $this->state(['config' => ['sections' => array_map(fn($section) => $section->create(), $sections)]]);
return $this->state(['config' => ['sections' => array_map(fn ($section) => $section->create(), $sections)]]);
}
/**

View File

@ -39,10 +39,6 @@ class ParticipantFactory extends Factory
return $this->state(['data' => $data]);
}
public function cancelled(): self {
return $this->state(['cancelled_at' => now()->subWeek()]);
}
public function nr(int $number): self
{
return $this->state(['member_id' => $number]);

View File

@ -1,30 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('forms', function (Blueprint $table) {
$table->string('zip')->nullable()->after('name');
$table->string('location')->nullable()->after('name');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('forms', function (Blueprint $table) {
$table->dropColumn('zip');
$table->dropColumn('location');
});
}
};

View File

@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('forms', function (Blueprint $table) {
$table->string('country')->nullable()->after('location');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('forms', function (Blueprint $table) {
$table->dropColumn('country');
});
}
};

View File

@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('forms', function (Blueprint $table) {
$table->json('leader_conditions')->after('name')->default(json_encode(['mode' => 'all', 'ifs' => []]));
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('forms', function (Blueprint $table) {
$table->dropColumn('leader_conditions');
});
}
};

View File

@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('participants', function (Blueprint $table) {
$table->datetime('cancelled_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('participants', function (Blueprint $table) {
$table->dropColumn('cancelled_at');
});
}
};

View File

@ -1,12 +0,0 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('form.replyToMail', '');
$this->migrator->add('prevention.replyToMail', '');
}
};

55
package-lock.json generated
View File

@ -27,10 +27,8 @@
"pusher-js": "^8.3.0",
"svg-sprite": "^2.0.2",
"typescript-eslint": "^8.34.0",
"uuid": "^11.1.0",
"vite": "^4.5.2",
"vue": "^3.3.4",
"vue-clipboard3": "^2.0.0",
"vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
"wnumb": "^1.2.0"
@ -1873,16 +1871,6 @@
"node": ">= 6"
}
},
"node_modules/clipboard": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -2151,11 +2139,6 @@
"node": ">=0.4.0"
}
},
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -2864,14 +2847,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -4099,11 +4074,6 @@
"node": ">=10"
}
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -4574,11 +4544,6 @@
"node": ">=0.8"
}
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@ -4865,18 +4830,6 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vinyl": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz",
@ -4986,14 +4939,6 @@
"vue": "^3.0.0 || ^2.0.0"
}
},
"node_modules/vue-clipboard3": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-clipboard3/-/vue-clipboard3-2.0.0.tgz",
"integrity": "sha512-Q9S7dzWGax7LN5iiSPcu/K1GGm2gcBBlYwmMsUc5/16N6w90cbKow3FnPmPs95sungns4yvd9/+JhbAznECS2A==",
"dependencies": {
"clipboard": "^2.0.6"
}
},
"node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",

View File

@ -44,10 +44,8 @@
"pusher-js": "^8.3.0",
"svg-sprite": "^2.0.2",
"typescript-eslint": "^8.34.0",
"uuid": "^11.1.0",
"vite": "^4.5.2",
"vue": "^3.3.4",
"vue-clipboard3": "^2.0.0",
"vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
"wnumb": "^1.2.0"

@ -1 +1 @@
Subproject commit 47f01b3c3c98821603f3612511d713cf51a6a14c
Subproject commit c20cf930f2037ddda4400dd7d35bf9ef707356eb

@ -1 +1 @@
Subproject commit f7b04591830ebdeaddf76236e4cbc87a8b3eec8f
Subproject commit 7304963370ff64fb5accf08da4864981cc424301

View File

@ -418,6 +418,11 @@ parameters:
count: 1
path: app/Mailgateway/Resources/MailgatewayResource.php
-
message: "#^Generic type Illuminate\\\\Pagination\\\\LengthAwarePaginator\\<int, App\\\\Member\\\\Member\\> in PHPDoc tag @return specifies 2 template types, but class Illuminate\\\\Pagination\\\\LengthAwarePaginator supports only 1\\: TValue$#"
count: 1
path: app/Member/Actions/SearchAction.php
-
message: "#^Class Plugins\\\\Test\\\\ServiceProvider not found\\.$#"
count: 1

View File

@ -31,12 +31,6 @@
@apply bg-yellow-500 text-yellow-100;
}
}
&.btn-default {
@apply bg-gray-700 text-gray-300;
&:not(.disabled):hover {
@apply bg-gray-500 text-gray-100;
}
}
&.btn-info {
@apply bg-blue-700 text-blue-300;
&:not(.disabled):hover {

View File

@ -5,14 +5,14 @@
.ce-settings__button,
.ce-toolbar__settings-btn,
.cdx-button,
.ce-popover__container,
.ce-popover,
.ce-toolbar__plus:hover {
@apply bg-primary-700;
color: inherit;
}
.cdx-search-field {
@apply bg-primary-600;
.ce-inline-tool-input {
@apply bg-primary-700 text-primary-200 placeholder-primary-500;
}
.ce-block--selected {

View File

@ -1,7 +1,3 @@
.v-popper__popper {
@apply max-w-lg;
}
.v-popper--theme-tooltip .v-popper__inner {
@apply bg-primary-400 text-primary-800 px-3 py-1 text-sm;
}

View File

@ -1,4 +0,0 @@
<svg x="0px" y="0px" viewBox="0 0 511.626 511.627" xml:space="preserve">
<path d="M392.857,292.354h-18.274c-2.669,0-4.859,0.855-6.563,2.573c-1.718,1.708-2.573,3.897-2.573,6.563v91.361 c0,12.563-4.47,23.315-13.415,32.262c-8.945,8.945-19.701,13.414-32.264,13.414H82.224c-12.562,0-23.317-4.469-32.264-13.414 c-8.945-8.946-13.417-19.698-13.417-32.262V155.31c0-12.562,4.471-23.313,13.417-32.259c8.947-8.947,19.702-13.418,32.264-13.418 h200.994c2.669,0,4.859-0.859,6.57-2.57c1.711-1.713,2.566-3.9,2.566-6.567V82.221c0-2.662-0.855-4.853-2.566-6.563 c-1.711-1.713-3.901-2.568-6.57-2.568H82.224c-22.648,0-42.016,8.042-58.102,24.125C8.042,113.297,0,132.665,0,155.313v237.542 c0,22.647,8.042,42.018,24.123,58.095c16.086,16.084,35.454,24.13,58.102,24.13h237.543c22.647,0,42.017-8.046,58.101-24.13 c16.085-16.077,24.127-35.447,24.127-58.095v-91.358c0-2.669-0.856-4.859-2.574-6.57 C397.709,293.209,395.519,292.354,392.857,292.354z"/>
<path d="M506.199,41.971c-3.617-3.617-7.905-5.424-12.85-5.424H347.171c-4.948,0-9.233,1.807-12.847,5.424 c-3.617,3.615-5.428,7.898-5.428,12.847s1.811,9.233,5.428,12.85l50.247,50.248L198.424,304.067 c-1.906,1.903-2.856,4.093-2.856,6.563c0,2.479,0.953,4.668,2.856,6.571l32.548,32.544c1.903,1.903,4.093,2.852,6.567,2.852 s4.665-0.948,6.567-2.852l186.148-186.148l50.251,50.248c3.614,3.617,7.898,5.426,12.847,5.426s9.233-1.809,12.851-5.426 c3.617-3.616,5.424-7.898,5.424-12.847V54.818C511.626,49.866,509.813,45.586,506.199,41.971z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M1.225 21.225A1.678 1.678 0 0 0 2.707 22H22.28a1.68 1.68 0 0 0 1.484-.775 1.608 1.608 0 0 0 .003-1.656L13.995 1.827a1.745 1.745 0 0 0-2.969 0l-9.8 17.742a1.603 1.603 0 0 0 0 1.656zm.859-1.143l9.82-17.773A.71.71 0 0 1 12.508 2a.73.73 0 0 1 .629.342l9.751 17.708a.626.626 0 0 1 .017.662.725.725 0 0 1-.626.288H2.708a.723.723 0 0 1-.623-.286.605.605 0 0 1-.001-.632zM13 15h-1V8h1zm-1.5 2.5a1 1 0 1 1 1 1 1.002 1.002 0 0 1-1-1z"/><path fill="none" d="M0 0h24v24H0z"/></svg>

Before

Width:  |  Height:  |  Size: 686 B

View File

@ -25,7 +25,6 @@ declare module 'vue' {
PageFullLayout: typeof import('@/components/page/FullLayout.vue')['default']
PageHeader: typeof import('@/components/page/Header.vue')['default']
PageLayout: typeof import('@/components/page/Layout.vue')['default']
PagePopups: typeof import('@/components/page/Popups.vue')['default']
PageSearchModal: typeof import('@/components/page/SearchModal.vue')['default']
PageTitle: typeof import('@/components/page/Title.vue')['default']
PageToolbarButton: typeof import('@/components/page/ToolbarButton.vue')['default']

View File

@ -1,21 +1,22 @@
<template>
<div>
<div class="flex flex-col group" :for="id" :class="sizeClass(size)">
<f-label v-if="label" :required="required" :value="label" />
<f-label v-if="label" :required="required" :value="label"></f-label>
<div class="relative w-full h-full">
<div :id="id" :class="[fieldAppearance, paddingX, paddingY]" />
<f-hint v-if="hint" :value="hint" />
<div :id="id" :class="[fieldAppearance, paddingX, paddingY]"></div>
<f-hint v-if="hint" :value="hint"></f-hint>
</div>
</div>
<ui-popup v-if="condition !== null"
heading="Bedingungen"
@close="
condition.resolve(condition.data);
condition = null;
"
<ui-popup
v-if="condition !== null"
heading="Bedingungen"
@close="
condition.resolve(condition.data);
condition = null;
"
>
<slot name="conditions" :data="condition.data" :resolve="condition.resolve" :reject="condition.reject" />
<slot name="conditions" :data="condition.data" :resolve="condition.resolve" :reject="condition.reject"></slot>
</ui-popup>
</div>
</template>
@ -105,10 +106,10 @@ class ConditionTune {
wrap(blockContent) {
this.wrapper = document.createElement('div');
const tooltip = document.createElement('div');
var tooltip = document.createElement('div');
tooltip.setAttribute('data-tooltip', '');
const content = document.createElement('div');
var content = document.createElement('div');
content.setAttribute('data-content', '');
content.appendChild(blockContent);
@ -143,7 +144,7 @@ class ConditionTune {
'Bedingung ' +
this.data.ifs
.map((i) => {
const parts = [i.field];
var parts = [i.field];
if (i.comparator === 'isEqual' || i.comparator === 'isIn') {
parts.push('=');
@ -190,7 +191,7 @@ class ConditionTune {
}
onMounted(async () => {
const tools = {
var tools = {
paragraph: {
class: Paragraph,
shortcut: 'CTRL+P',
@ -224,7 +225,7 @@ onMounted(async () => {
},
};
const tunes = [];
var tunes = [];
if (props.conditions) {
tools.condition = {

View File

@ -1,21 +0,0 @@
<template>
<ui-popup v-for="(popup, index) in swal.popups" :key="index" :icon="popup.icon" :heading="popup.title" @close="popup.reject(popup.id)">
<div class="text-center mt-4" v-text="popup.body" />
<div class="mt-4">
<template v-for="field in popup.fields">
<f-text v-if="field.type === 'text'" :id="field.name" :key="field.name" v-model="popup.payload[field.name]" :name="field.name" :label="field.label" />
<f-select v-if="field.type === 'select'" :id="field.name" :key="field.name" v-model="popup.payload[field.name]" :name="field.name" :label="field.label" :options="field.options" />
</template>
</div>
<div class="flex justify-center space-x-4 mt-6">
<ui-button type="button" class="btn-primary" @click.prevent="popup.resolve(popup.id)">{{ popup.confirmButton }}</ui-button>
<ui-button type="button" class="btn-default" @click.prevent="popup.reject(popup.id)">{{ popup.cancelButton }}</ui-button>
</div>
</ui-popup>
</template>
<script lang="ts" setup>
import useSwal from '@/stores/swalStore.ts';
const swal = useSwal();
</script>

View File

@ -1,14 +1,28 @@
<template>
<a v-tooltip="tooltip" :href="href" :target="blank ? '_BLANK' : '_SELF'" class="inline-flex btn btn-sm">
<ui-sprite :src="icon" />
<ui-sprite :src="icon"></ui-sprite>
</a>
</template>
<script lang="ts" setup>
withDefaults(defineProps<{
tooltip: string;
href?: string;
blank?: boolean,
icon: string,
}>(), {href: '#', blank: false});
<script setup>
defineProps({
tooltip: {
required: true,
type: String,
},
href: {
type: String,
default: () => '#',
required: false,
},
blank: {
type: Boolean,
default: () => false,
required: false,
},
icon: {
type: String,
required: true,
},
});
</script>

View File

@ -1,22 +1,24 @@
<template>
<button v-bind="$attrs" class="btn btn-primary relative group">
<div :class="{hidden: !isLoading, flex: isLoading}" class="absolute items-center top-0 h-full left-0 ml-2">
<ui-spinner class="border-primary-400 w-6 h-6 group-hover:border-primary-200" />
<ui-spinner class="border-primary-400 w-6 h-6 group-hover:border-primary-200"></ui-spinner>
</div>
<slot />
<slot></slot>
</button>
</template>
<script>
import {menuStore} from '../../stores/menuStore.js';
export default {
data: function () {
return {};
},
props: {
isLoading: {
type: Boolean,
default: false,
},
},
data: function () {
return {};
},
};
</script>

View File

@ -1,25 +1,23 @@
<template>
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6 bg-black/60 backdrop-blur-sm">
<div class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto"
:class="full ? 'h-full' : innerWidth"
<div
class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto"
:class="full ? 'h-full' : innerWidth"
>
<div class="absolute top-0 right-0 mt-6 mr-6 flex space-x-6">
<slot name="actions" />
<slot name="actions"></slot>
<a href="#" @click.prevent="$emit('close')">
<ui-sprite src="close" class="text-zinc-400 w-6 h-6" />
<ui-sprite src="close" class="text-zinc-400 w-6 h-6"></ui-sprite>
</a>
</div>
<div class="flex justify-center">
<ui-sprite v-if="icon" class="text-yellow-700 size-28" :src="icon" />
</div>
<h3 v-if="heading" class="font-semibold text-primary-200 text-xl" :class="{'text-center mt-5': icon !== null}" v-html="heading" />
<h3 v-if="heading" class="font-semibold text-primary-200 text-xl" v-html="heading"></h3>
<div class="text-primary-100 group is-popup grow flex flex-col">
<suspense>
<div>
<slot />
<slot></slot>
</div>
<template #fallback>
<ui-loading />
<ui-loading></ui-loading>
</template>
</suspense>
</div>
@ -27,14 +25,21 @@
</div>
</template>
<script lang="ts" setup>
defineEmits<{
close: [],
}>();
withDefaults(defineProps<{
heading?: string,
innerWidth?: string,
full?: boolean,
icon?: string|null,
}>(), {innerWidth: 'max-w-xl', full: false, heading: '', icon: null});
<script>
export default {
props: {
heading: {
type: String,
default: () => '',
},
innerWidth: {
default: () => 'max-w-xl',
type: String,
},
full: {
type: Boolean,
default: () => false,
},
},
};
</script>

View File

@ -1,15 +1,15 @@
<template>
<ui-popup v-if="selecting !== false" heading="Resource auswählen" @close="selecting = false">
<ui-remote-selector :value="selecting" @input="set" />
<ui-remote-selector :value="selecting" @input="set"></ui-remote-selector>
</ui-popup>
<label class="flex flex-col group" :for="id" :class="sizeClass(size)">
<f-label v-if="label" :required="false" :value="label" />
<f-label v-if="label" :required="false" :value="label"></f-label>
<div class="relative flex-none flex">
<div class="w-full flex flex-col justify-center" :class="[fieldHeight, fieldAppearance, paddingX]" @click.prevent="selecting = modelValue === null ? null : {...modelValue}">
<div v-if="modelValue !== null" v-text="modelValue.resource" />
<div v-if="modelValue !== null" v-text="modelValue.resource"></div>
<div v-else>Datei auswählen</div>
</div>
<f-hint v-if="hint" :value="hint" />
<f-hint v-if="hint" :value="hint"></f-hint>
</div>
</label>
</template>

View File

@ -47,10 +47,8 @@ export function useApiIndex(firstUrl, siteName = null) {
single.value = null;
}
async function remove(model, force = true) {
await axios.delete(model.links.destroy, {
headers: { 'X-Force': force ? '1' : '0' }
});
async function remove(model) {
await axios.delete(model.links.destroy);
await reload();
}

View File

@ -1,14 +0,0 @@
import { Axios } from 'axios';
import { inject } from 'vue';
export default function() {
const axios = inject<Axios>('axios');
async function download(url: string, payload: Record<string, string>) {
const payloadString = btoa(encodeURIComponent(JSON.stringify(payload)));
await axios.get(`${url}?payload=${payloadString}&validate=1`);
window.open(`${url}?payload=${payloadString}`);
}
return { download };
}

View File

@ -1,12 +1,13 @@
<template>
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3" />
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
<!-- ******************************** Sidebar ******************************** -->
<div class="fixed z-40 bg-gray-800 p-6 w-56 top-0 h-screen border-r border-gray-600 border-solid flex flex-col justify-between transition-all"
:class="{
'-left-[14rem]': !menuStore.isShifted,
'left-0': menuStore.isShifted,
}"
<div
class="fixed z-40 bg-gray-800 p-6 w-56 top-0 h-screen border-r border-gray-600 border-solid flex flex-col justify-between transition-all"
:class="{
'-left-[14rem]': !menuStore.isShifted,
'left-0': menuStore.isShifted,
}"
>
<div class="grid gap-2">
<v-link href="/" menu="dashboard" icon="loss">Dashboard</v-link>
@ -28,15 +29,13 @@
<v-link icon="logout" href="/logout" @click.prevent="$inertia.post('/logout')">Abmelden</v-link>
</div>
<a v-if="menuStore.hideable" href="#" class="absolute right-0 top-0 mr-2 mt-2" @click.prevent="menuStore.hide()">
<ui-sprite src="close" class="w-5 h-5 text-gray-300" />
<ui-sprite src="close" class="w-5 h-5 text-gray-300"></ui-sprite>
</a>
</div>
<page-popups />
<slot></slot>
<slot />
<page-search-modal v-if="searchVisible" @close="searchVisible = false" />
<page-search-modal v-if="searchVisible" @close="searchVisible = false"></page-search-modal>
</template>
<script>

View File

@ -17,4 +17,4 @@ var options = {
},
};
export { Plugin, options };
export {Plugin, options};

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { router } from '@inertiajs/vue3';
import {defineStore} from 'pinia';
import {router} from '@inertiajs/vue3';
export const menuStore = defineStore('menu', {
state: () => ({

View File

@ -1,93 +0,0 @@
import { defineStore } from 'pinia';
import { v4 as uuidv4 } from 'uuid';
type Payload = Record<string, string|null>;
interface Popup {
id: string;
title: string;
body: string;
icon: string|null;
confirmButton: string;
cancelButton: string;
resolve: (id: string) => void;
reject: (id: string) => void;
fields: SwalField[];
payload: Payload;
}
interface SwalField {
name: string;
label: string;
required: boolean;
type: 'select' | 'text';
options: [],
}
export default defineStore('swal', {
state: () => ({
popups: [] as Popup[],
}),
actions: {
confirm(title: string, body: string): Promise<void> {
return new Promise((resolve, reject) => {
new Promise<string>((resolve, reject) => {
this.popups.push({
title,
body,
confirmButton: 'Okay',
cancelButton: 'Abbrechen',
resolve,
reject,
id: uuidv4(),
icon: 'warning-triangle-light',
fields: [],
payload: {},
});
}).then((id) => {
this.remove(id);
resolve();
}).catch((id) => {
this.remove(id);
reject();
});
});
},
ask(title: string, body: string, fields: SwalField[] = []): Promise<Payload> {
return new Promise<Payload>((resolve, reject) => {
new Promise<string>((resolve, reject) => {
const payload: Payload = {};
fields.forEach(f => payload[f.name] = null);
this.popups.push({
title,
body,
confirmButton: 'Okay',
cancelButton: 'Abbrechen',
resolve,
reject,
id: uuidv4(),
icon: 'warning-triangle-light',
fields: fields,
payload: payload,
});
}).then((id) => {
const p = this.find(id)?.payload;
this.remove(id);
resolve(p || {});
}).catch((id) => {
this.remove(id);
reject();
});
});
},
remove(id: string) {
this.popups = this.popups.filter(p => p.id !== id);
},
find(id: string): Popup|undefined {
return this.popups.find(p => p.id === id);
}
},
});

View File

@ -4,38 +4,40 @@
<page-toolbar-button :href="meta.links.index" color="primary" icon="undo">zurück</page-toolbar-button>
</template>
<template #right>
<f-save-button form="actionform" />
<f-save-button form="actionform"></f-save-button>
</template>
<form id="actionform" class="grow p-3" @submit.prevent="submit">
<ui-popup v-if="mode === 'edit' && currentSubactivity !== null" heading="Neue Untertätigkeit" @close="currentSubactivity = null">
<subactivity-form v-if="currentSubactivity" class="mt-4" :value="currentSubactivity" @stored="reloadSubactivities" @updated="mergeSubactivity" />
<ui-popup heading="Neue Untertätigkeit" v-if="mode === 'edit' && currentSubactivity !== null" @close="currentSubactivity = null">
<subactivity-form class="mt-4" v-if="currentSubactivity" :value="currentSubactivity" @stored="reloadSubactivities" @updated="mergeSubactivity"></subactivity-form>
</ui-popup>
<div class="flex space-x-3">
<f-text id="name" v-model="inner.name" label="Name" required />
<f-switch id="is_filterable" v-model="inner.is_filterable" name="is_filterable" label="Filterbar" />
<f-text id="name" v-model="inner.name" label="Name" required></f-text>
<f-switch v-model="inner.is_filterable" name="is_filterable" id="is_filterable" label="Filterbar"></f-switch>
</div>
<div class="flex space-x-3 items-center mt-6 mb-2">
<f-checkboxes-label>Untertätigkeiten</f-checkboxes-label>
<ui-icon-button v-if="mode === 'edit'" icon="plus" @click.prevent="currentSubactivity = inner.subactivity_model">Neu</ui-icon-button>
<ui-icon-button icon="plus" v-if="mode === 'edit'" @click.prevent="currentSubactivity = inner.subactivity_model">Neu</ui-icon-button>
</div>
<div class="grid gap-2 sm:grid-cols-2 md:grid-cols-4">
<div v-for="option in subactivities" class="flex items-center space-x-2">
<a v-if="mode === 'edit'"
href="#"
class="transition hover:bg-yellow-600 group w-5 h-5 rounded-full flex items-center justify-center flex-none"
@click.prevent="currentSubactivity = option"
<a
href="#"
v-if="mode === 'edit'"
@click.prevent="currentSubactivity = option"
class="transition hover:bg-yellow-600 group w-5 h-5 rounded-full flex items-center justify-center flex-none"
>
<ui-sprite src="pencil" class="text-yellow-800 w-3 h-3 group-hover:text-yellow-200 transition" />
<ui-sprite src="pencil" class="text-yellow-800 w-3 h-3 group-hover:text-yellow-200 transition"></ui-sprite>
</a>
<f-switch :id="`subactivities-${option.id}`"
:key="option.id"
v-model="inner.subactivities"
inline
size="sm"
name="subactivities[]"
:value="option.id"
:label="option.name"
/>
<f-switch
inline
size="sm"
:key="option.id"
v-model="inner.subactivities"
name="subactivities[]"
:id="`subactivities-${option.id}`"
:value="option.id"
:label="option.name"
></f-switch>
</div>
</div>
</form>
@ -47,11 +49,6 @@ import {defineAsyncComponent} from 'vue';
import {useToast} from 'vue-toastification';
export default {
props: {
data: {},
meta: {},
},
setup() {
const toast = useToast();
@ -66,6 +63,11 @@ export default {
};
},
props: {
data: {},
meta: {},
},
components: {
'subactivity-form': defineAsyncComponent(() => import('./SubactivityForm.vue')),
},

View File

@ -1,42 +1,42 @@
<template>
<page-layout>
<form target="_BLANK" class="max-w-4xl w-full mx-auto gap-6 grid-cols-2 grid p-6">
<f-text id="eventName" v-model="values.eventName" class="col-span-2" label="Veranstaltungs-Name" required />
<f-text id="dateFrom" v-model="values.dateFrom" type="date" label="Datum von" required />
<f-text id="dateUntil" v-model="values.dateUntil" type="date" label="Datum bis" required />
<f-text id="eventName" v-model="values.eventName" class="col-span-2" label="Veranstaltungs-Name" required></f-text>
<f-text id="dateFrom" v-model="values.dateFrom" type="date" label="Datum von" required></f-text>
<f-text id="dateUntil" v-model="values.dateUntil" type="date" label="Datum bis" required></f-text>
<f-text id="zipLocation" v-model="values.zipLocation" label="PLZ / Ort" required />
<f-select id="country" v-model="values.country" :options="countries" name="country" label="Land" required />
<f-text id="zipLocation" v-model="values.zipLocation" label="PLZ / Ort" required></f-text>
<f-select id="country" v-model="values.country" :options="countries" name="country" label="Land" required></f-select>
<div class="border-gray-200 shadow shadow-primary-700 p-3 shadow-[0_0_4px_gray] col-span-2">
<f-text id="search_text" ref="searchInput" v-model="searchString" class="col-span-2" label="Suchen …" size="sm" @keypress.enter.prevent="onSubmitFirstMemberResult" />
<f-text id="search_text" ref="searchInput" v-model="searchString" class="col-span-2" label="Suchen …" size="sm" @keypress.enter.prevent="onSubmitFirstMemberResult"></f-text>
<div class="mt-2 grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-2 col-span-2">
<f-switch v-for="member in results.hits"
:id="`members-${member.id}`"
:key="member.id"
v-model="values.members"
:label="member.fullname"
name="members[]"
:value="member.id"
size="sm"
inline
@keypress.enter.prevent="onSubmitMemberResult(member)"
/>
<f-switch
v-for="member in results.hits"
:id="`members-${member.id}`"
:key="member.id"
v-model="values.members"
:label="member.fullname"
name="members[]"
:value="member.id"
size="sm"
inline
@keypress.enter.prevent="onSubmitMemberResult(member)"
></f-switch>
</div>
</div>
<button v-for="(compiler, index) in compilers" :key="index" class="btn btn-primary mt-3 inline-block" @click.prevent="download('/contribution-generate', {...values, type: compiler.id})" v-text="`Für ${compiler.name} erstellen`" />
<button v-for="(compiler, index) in compilers" :key="index" class="btn btn-primary mt-3 inline-block" @click.prevent="submit(compiler.class)" v-text="compiler.title"></button>
</form>
</page-layout>
</template>
<script lang="js" setup>
import { ref } from 'vue';
import { ref, inject } from 'vue';
import useSearch from '../../composables/useSearch.js';
import useDownloads from '@/composables/useDownloads.ts';
const axios = inject('axios');
const { searchString, results, clearSearch } = useSearch(['birthday IS NOT NULL', 'address IS NOT EMPTY']);
const {download} = useDownloads();
const props = defineProps({
data: {},
@ -56,6 +56,12 @@ const values = ref({
...props.data,
});
async function submit(compiler) {
values.value.type = compiler;
await axios.post('/contribution-validate', values.value);
var payload = btoa(encodeURIComponent(JSON.stringify(values.value)));
window.open(`/contribution-generate?payload=${payload}`);
}
function onSubmitMemberResult(selected) {
if (values.value.members.find((m) => m === selected.id) !== undefined) {
values.value.members = values.value.members.filter((m) => m !== selected.id);

View File

@ -1,49 +1,54 @@
<template>
<ui-note v-if="locked" class="mt-2" type="danger">
Dieses Formular wurde bereits bearbeitet.<br>
Dieses Formular wurde bereits bearbeitet.<br />
Bitte speichere es erst ab und editiere dann die Bedingungen.
</ui-note>
<div v-else>
<f-select :id="`mode-${id}`" :name="`mode-${id}`" :model-value="modelValue.mode" :options="modeOptions" label="Modus" @update:model-value="changeMode" />
<f-select :id="`mode-${id}`" :name="`mode-${id}`" :model-value="modelValue.mode" :options="modeOptions" label="Modus" @update:model-value="changeMode"></f-select>
<ui-icon-button class="mt-4 mb-2" icon="plus" @click="addCondition">Bedingung einfügen</ui-icon-button>
<div v-for="(condition, index) in modelValue.ifs" :key="index" class="grid grid-cols-[1fr_1fr_1fr_max-content] gap-2">
<f-select :id="`field-${index}-${id}`"
:model-value="condition.field"
:options="fieldOptions"
:name="`field-${index}-${id}`"
label="Feld"
@update:model-value="update(index, 'field', $event)"
/>
<f-select :id="`comparator-${index}-${id}`"
:options="comparatorOptions"
:model-value="condition.comparator"
:name="`comparator-${index}-${id}`"
label="Vergleich"
@update:model-value="update(index, 'comparator', $event)"
/>
<f-select v-if="condition.field && ['isEqual', 'isNotEqual'].includes(condition.comparator) && ['RadioField', 'DropdownField', 'GroupField'].includes(getField(condition.field).type)"
:id="`value-${index}-${id}`"
v-model="condition.value"
:options="getOptions(condition.field)"
:name="`value-${index}-${id}`"
label="Wert"
/>
<f-multipleselect v-if="condition.field && ['isIn', 'isNotIn'].includes(condition.comparator) && ['RadioField', 'DropdownField', 'GroupField'].includes(getField(condition.field).type)"
:id="`value-${index}-${id}`"
v-model="condition.value"
:options="getOptions(condition.field)"
label="Wert"
/>
<f-switch v-if="condition.field && condition.comparator && ['CheckboxField'].includes(getField(condition.field).type)"
:id="`value-${index}-${id}`"
v-model="condition.value"
:name="`value-${index}-${id}`"
label="Wert"
/>
<ui-action-button tooltip="Löschen" icon="trash" class="btn-danger self-end h-8" @click="remove(index)" />
<f-select
:id="`field-${index}-${id}`"
:model-value="condition.field"
:options="fieldOptions"
:name="`field-${index}-${id}`"
label="Feld"
@update:model-value="update(index, 'field', $event)"
></f-select>
<f-select
:id="`comparator-${index}-${id}`"
:options="comparatorOptions"
:model-value="condition.comparator"
:name="`comparator-${index}-${id}`"
label="Vergleich"
@update:model-value="update(index, 'comparator', $event)"
></f-select>
<f-select
v-if="condition.field && ['isEqual', 'isNotEqual'].includes(condition.comparator) && ['RadioField', 'DropdownField', 'GroupField'].includes(getField(condition.field).type)"
:id="`value-${index}-${id}`"
v-model="condition.value"
:options="getOptions(condition.field)"
:name="`value-${index}-${id}`"
label="Wert"
></f-select>
<f-multipleselect
v-if="condition.field && ['isIn', 'isNotIn'].includes(condition.comparator) && ['RadioField', 'DropdownField', 'GroupField'].includes(getField(condition.field).type)"
:id="`value-${index}-${id}`"
v-model="condition.value"
:options="getOptions(condition.field)"
label="Wert"
></f-multipleselect>
<f-switch
v-if="condition.field && condition.comparator && ['CheckboxField'].includes(getField(condition.field).type)"
:id="`value-${index}-${id}`"
v-model="condition.value"
:name="`value-${index}-${id}`"
label="Wert"
></f-switch>
<ui-action-button tooltip="Löschen" icon="trash" class="btn-danger self-end h-8" @click="remove(index)"></ui-action-button>
</div>
</div>
</template>
@ -98,9 +103,9 @@ function changeMode(mode) {
}
function update(index, key, value) {
const inner = {...props.modelValue};
var inner = {...props.modelValue};
if (key === 'comparator') {
const old = inner.ifs[index];
var old = inner.ifs[index];
inner.ifs[index] = {
field: old.field,
comparator: value,
@ -108,6 +113,7 @@ function update(index, key, value) {
};
}
if (key === 'field') {
var old = inner.ifs[index];
inner.ifs[index] = {
field: value,
comparator: null,

View File

@ -5,6 +5,22 @@
<page-toolbar-button color="primary" icon="plus" @click.prevent="create">Veranstaltung erstellen</page-toolbar-button>
</template>
<ui-popup v-if="deleting !== null" :heading="`Veranstaltung ${deleting.name} löschen?`" @close="deleting = null">
<div>
<p class="mt-4">Diese Veranstaltung löschen?</p>
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#"
class="text-center btn btn-danger"
@click.prevent="
remove(deleting);
deleting = null;
"
>Veranstaltung löschen</a>
<a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a>
</div>
</div>
</ui-popup>
<ui-popup v-if="single !== null && single.config === null" heading="Vorlage auswählen" @close="cancel">
<div class="mt-3 grid gap-3 grid-cols-2">
<a v-for="(template, index) in meta.templates" :key="index" class="py-2 px-3 border rounded bg-zinc-800 hover:bg-zinc-700 transition" href="#" @click.prevent="setTemplate(template)">
@ -20,15 +36,14 @@
<ui-popup v-if="single !== null && single.config !== null" :heading="`Veranstaltung ${single.id ? 'bearbeiten' : 'erstellen'}`" full @close="cancel">
<div class="flex flex-col mt-3">
<ui-tabs v-model="active" :entries="tabs" />
<div v-show="active === 0" class="grid grid-cols-4 gap-3">
<div class="flex space-x-3 col-span-2">
<div v-show="active === 0" class="grid grid-cols-2 gap-3">
<div class="flex space-x-3">
<f-text id="name" v-model="single.name" class="grow" label="Name" required />
<f-switch id="is_active" v-model="single.is_active" name="is_active" label="Aktiv" hint="Inaktive Veranstaltungen werden außerhalb von Adrema wie nicht existierende Veranstaltungen betrachtet. Insbesondere ist eine Anmeldung dann nicht möglich und die Veranstaltung erscheint auch nicht in der Veranstaltungs-Übersicht." />
<f-switch id="is_private" v-model="single.is_private" name="is_private" label="Privat" hint="Ist eine Veranstaltung privat, so wird diese nicht auf der Website angezeigt. Eine Anmeldung ist jedoch trotzdem möglich, wenn man über den Anmelde-Link verfügt." />
<f-switch id="is_active" v-model="single.is_active" name="is_active" label="Aktiv" />
<f-switch id="is_private" v-model="single.is_private" name="is_private" label="Privat" />
</div>
<f-singlefile id="header_image"
v-model="single.header_image"
class="col-span-2"
label="Bild"
name="header_image"
parent-name="form"
@ -38,17 +53,13 @@
/>
<f-text id="from" v-model="single.from" type="date" label="Von" required />
<f-text id="to" v-model="single.to" type="date" label="Bis" required />
<f-text id="zip" v-model="single.zip" label="PLZ" />
<f-text id="location" v-model="single.location" label="Ort" />
<f-select id="country" v-model="single.country" class="col-span-2" name="country" label="Land" :options="meta.countries" />
<f-text id="registration_from" v-model="single.registration_from" type="datetime-local" label="Registrierung von" hint="Ist eine Anmeldung laut dieser zwei Datumsangaben möglich, kann man sich anmelden. Andernfalls wird die Veranstaltung (mit Beschreibungstext) auf der Übersichtsseite angezeigt, man kommt allerdings nicht zum Anmeldeformular." required />
<f-text id="registration_from" v-model="single.registration_from" type="datetime-local" label="Registrierung von" required />
<f-text id="registration_until" v-model="single.registration_until" type="datetime-local" label="Registrierung bis" required />
<f-textarea id="excerpt"
v-model="single.excerpt"
hint="Gebe hier eine kurze Beschreibung für die Veranstaltungs-Übersicht ein (Maximal 130 Zeichen)."
label="Auszug"
:rows="5"
class="col-span-full"
required
/>
</div>
@ -69,13 +80,13 @@
<div>
<ui-tabs v-model="activeMailTab" :entries="mailTabs" />
<f-editor v-if="activeMailTab === 0" id="mail_top" v-model="single.mail_top" name="mail_top" label="E-Mail-Teil 1" :rows="8" conditions required>
<template #conditions="{cData, resolve}">
<conditions-form id="mail_top_conditions" :single="single" :value="cData" @save="resolve" />
<template #conditions="{data, resolve}">
<conditions-form id="mail_top_conditions" :single="single" :value="data" @save="resolve" />
</template>
</f-editor>
<f-editor v-if="activeMailTab === 1" id="mail_bottom" v-model="single.mail_bottom" name="mail_bottom" label="E-Mail-Teil 2" :rows="8" conditions required>
<template #conditions="{d, resolve}">
<conditions-form id="mail_bottom_conditions" :single="single" :value="d" @save="resolve" />
<template #conditions="{data, resolve}">
<conditions-form id="mail_bottom_conditions" :single="single" :value="data" @save="resolve" />
</template>
</f-editor>
</div>
@ -115,11 +126,6 @@
<conditions id="prevention_conditions" v-model="single.prevention_conditions" :single="single" />
</ui-box>
</div>
<div v-if="active === 6">
<ui-box heading="Bedingung für Leiter*in">
<conditions id="leader_conditions" v-model="single.leader_conditions" :single="single" />
</ui-box>
</div>
</div>
<template #actions>
<a href="#" @click.prevent="submit">
@ -146,7 +152,6 @@
<th>Name</th>
<th>Von</th>
<th>Bis</th>
<th>Tags</th>
<th>Anzahl TN</th>
<th />
</tr>
@ -163,13 +168,6 @@
<td>
<div v-text="form.to_human" />
</td>
<td>
<div class="bool-row">
<ui-bool true-comment="aktiv" false-comment="inaktiv" :value="form.is_active">A</ui-bool>
<ui-bool true-comment="private Veranstaltung" false-comment="nicht private Veranstaltung" :value="form.is_private">P</ui-bool>
<ui-bool true-comment="Anmeldung möglich (lt. 'Registrierung von / bis')" false-comment="Anmeldeschluss erreicht" :value="form.is_in_dates">D</ui-bool>
</div>
</td>
<td>
<div v-text="form.participants_count" />
</td>
@ -179,10 +177,8 @@
<ui-action-button tooltip="Teilnehmende anzeigen" class="btn-info" icon="user" @click.prevent="showParticipants(form)" />
<ui-action-button :href="form.links.frontend" target="_BLANK" tooltip="zur Anmeldeseite" class="btn-info" icon="eye" />
<ui-action-button tooltip="Kopieren" class="btn-info" icon="copy" @click="onCopy(form)" />
<ui-action-button tooltip="Nachmelde-Link kopieren" class="btn-info" icon="externallink" @click="copyLaterLink(form)" />
<ui-action-button tooltip="Zuschuss-Liste erstellen" class="btn-info" icon="contribution" @click="onGenerateContribution(form)" />
<ui-action-button :href="form.links.export" target="_BLANK" tooltip="als Tabellendokument exportieren" class="btn-info" icon="document" />
<ui-action-button tooltip="Löschen" class="btn-danger" icon="trash" @click.prevent="onDelete(form)" />
<ui-action-button tooltip="Löschen" class="btn-danger" icon="trash" @click.prevent="deleting = form" />
</div>
</td>
</tr>
@ -202,25 +198,20 @@ import Participants from './Participants.vue';
import Conditions from './Conditions.vue';
import ConditionsForm from './ConditionsForm.vue';
import { useToast } from 'vue-toastification';
import useSwal from '@/stores/swalStore.ts';
import useDownloads from '@/composables/useDownloads.ts';
import useClipboard from 'vue-clipboard3';
const props = defineProps(indexProps);
const { meta, data, reloadPage, reload, create, single, edit, cancel, submit, remove, getFilter, setFilter } = useIndex(props.data, 'form');
const { meta, data, reloadPage, create, single, edit, cancel, submit, remove, getFilter, setFilter } = useIndex(props.data, 'form');
const axios = inject('axios');
const toast = useToast();
const {download} = useDownloads();
const {toClipboard} = useClipboard();
const deleting = ref(null);
const showing = ref(null);
const fileSettingPopup = ref(null);
const active = ref(0);
const activeMailTab = ref(0);
const tabs = [{ title: 'Allgemeines' }, { title: 'Beschreibung' }, { title: 'Formular' }, { title: 'Bestätigungs-E-Mail' }, { title: 'Export' }, { title: 'Prävention' }, {title: 'Zuschüsse'}];
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 swal = useSwal();
const allFields = computed(() => {
if (!single.value) {
@ -237,29 +228,9 @@ const allFields = computed(() => {
return result;
});
async function onCopy(form) {
await swal.confirm('Diese Veranstaltung kopieren?', 'Nach dem Kopieren wird die Veranstaltung auf inaktiv gesetzt. Bitte aktiviere den Filter "inaktive zeigen", um die kopierte Veranstaltung zu sehen.');
await axios.post(form.links.copy, {});
reload(false);
}
async function onGenerateContribution(form) {
const response = await swal.ask('Zuschussliste erstellen', 'Hiermit erstellst du eine Zuschussliste mit allen angemeldeten Mitgliedern. Bite wähle aus, für welche Organisation du eine Liste erstellen willst.', [
{
name: 'type',
label: 'Organisation',
required: true,
type: 'select',
options: meta.value.contribution_types,
}
]);
await download(form.links.contribution, {type: response.type, validate: '1'});
await download(form.links.contribution, {type: response.type});
}
async function onDelete(form) {
await swal.confirm('Diese Veranstaltung löschen?', `Die Veranstaltung ${form.name} wird gelöscht werden.`);
await remove(form);
function onCopy(form) {
single.value = {...meta.value.default, ...form};
single.value.id = null;
}
function setTemplate(template) {
@ -269,12 +240,6 @@ function setTemplate(template) {
single.value.mail_bottom = template.mail_bottom;
}
async function copyLaterLink(form) {
const response = await axios.get(form.links.laterlink);
await toClipboard(response.data.url);
toast.success('Link in Zwischenablage kopiert');
}
async function saveFileConditions(conditions) {
await axios.patch(`/mediaupload/${fileSettingPopup.value.id}`, {
properties: {

View File

@ -1,20 +1,20 @@
<template>
<div>
<ui-popup v-if="editing !== null" heading="Mitglied bearbeiten" closeable full @close="editing = null">
<event-form :value="editing.preview"
:base-url="meta.base_url"
style="--primary: hsl(181, 75%, 26%); --secondary: hsl(181, 75%, 35%); --font: hsl(181, 84%, 78%); --circle: hsl(181, 86%, 16%)"
as-form
@save="updateParticipant($event.detail[0])"
/>
<event-form
:value="editing.preview"
:base-url="meta.base_url"
style="--primary: hsl(181, 75%, 26%); --secondary: hsl(181, 75%, 35%); --font: hsl(181, 84%, 78%); --circle: hsl(181, 86%, 16%)"
as-form
@save="updateParticipant($event.detail[0])"
></event-form>
</ui-popup>
<ui-popup v-if="assigning !== null" heading="Mitglied zuweisen" closeable @close="assigning = null">
<member-assign @assign="assign" />
<member-assign @assign="assign"></member-assign>
</ui-popup>
<ui-popup v-if="deleting !== null" heading="Teilnehmer*in abmelden?" @close="deleting = null">
<ui-popup v-if="deleting !== null" heading="Teilnehmer*in löschen?" @close="deleting = null">
<div>
<p class="mt-4">Den*Die Teilnehmer*in abmelden?</p>
<f-switch class="mt-2" v-model="deleting.force" name="force_delete" id="force_delete" label="löschen statt abmelden (permanent)" size="sm" />
<p class="mt-4">Den*Die Teilnehmer*in löschen?</p>
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" class="text-center btn btn-danger" @click.prevent="handleDelete">Mitglied loschen</a>
<a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a>
@ -23,59 +23,62 @@
</ui-popup>
<page-filter>
<template #buttons>
<f-text id="search" v-model="innerFilter.search" name="search" label="Suchen" size="sm" />
<f-text id="search" v-model="innerFilter.search" name="search" label="Suchen" size="sm"></f-text>
<ui-icon-button icon="plus" @click="editing = {participant: null, preview: JSON.stringify(meta.form_config)}">Hinzufügen</ui-icon-button>
<f-switch v-if="meta.has_nami_field" id="group_participants" v-model="groupParticipants" label="Gruppieren" size="sm" name="group_participants" />
<f-multipleselect id="active_columns" v-model="activeColumnsConfig" :options="meta.columns" label="Aktive Spalten" size="sm" />
<f-switch v-if="meta.has_nami_field" id="group_participants" v-model="groupParticipants" label="Gruppieren" size="sm" name="group_participants"></f-switch>
<f-multipleselect id="active_columns" v-model="activeColumnsConfig" :options="meta.columns" label="Aktive Spalten" size="sm"></f-multipleselect>
</template>
<template #fields>
<f-switch id="show_cancelled" v-model="innerFilter.show_cancelled" label="Abgemeldete zeigen" size="sm" name="show_cancelled" />
<template v-for="(filter, index) in meta.filters">
<f-select v-if="filter.base_type === 'CheckboxField'"
:id="`filter-field-${index}`"
:key="`filter-field-${index}`"
v-model="innerFilter.data[filter.key]"
:null-value="meta.default_filter_value"
:name="`filter-field-${index}`"
:options="checkboxFilterOptions"
:label="filter.name"
size="sm"
/>
<f-select v-if="filter.base_type === 'DropdownField'"
:id="`filter-field-${index}`"
:key="`filter-field-${index}`"
v-model="innerFilter.data[filter.key]"
:null-value="meta.default_filter_value"
:name="`filter-field-${index}`"
:options="dropdownFilterOptions(filter)"
:label="filter.name"
size="sm"
/>
<f-select v-if="filter.base_type === 'RadioField'"
:id="`filter-field-${index}`"
:key="`filter-field-${index}`"
v-model="innerFilter.data[filter.key]"
:null-value="meta.default_filter_value"
:name="`filter-field-${index}`"
:options="dropdownFilterOptions(filter)"
:label="filter.name"
size="sm"
/>
<f-select
v-if="filter.base_type === 'CheckboxField'"
:id="`filter-field-${index}`"
:key="`filter-field-${index}`"
v-model="innerFilter.data[filter.key]"
:null-value="meta.default_filter_value"
:name="`filter-field-${index}`"
:options="checkboxFilterOptions"
:label="filter.name"
size="sm"
></f-select>
<f-select
v-if="filter.base_type === 'DropdownField'"
:id="`filter-field-${index}`"
:key="`filter-field-${index}`"
v-model="innerFilter.data[filter.key]"
:null-value="meta.default_filter_value"
:name="`filter-field-${index}`"
:options="dropdownFilterOptions(filter)"
:label="filter.name"
size="sm"
></f-select>
<f-select
v-if="filter.base_type === 'RadioField'"
:id="`filter-field-${index}`"
:key="`filter-field-${index}`"
v-model="innerFilter.data[filter.key]"
:null-value="meta.default_filter_value"
:name="`filter-field-${index}`"
:options="dropdownFilterOptions(filter)"
:label="filter.name"
size="sm"
></f-select>
</template>
</template>
</page-filter>
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
<thead>
<ui-th v-for="column in activeColumns"
:key="column.id"
:column="column.id"
:label="column.name"
:value="getSort"
sortable
@update:model-value="setSort(column.id, {filter: toFilterString(innerFilter)})"
/>
<th />
<ui-th
v-for="column in activeColumns"
:key="column.id"
:column="column.id"
:label="column.name"
:value="getSort"
sortable
@update:model-value="setSort(column.id, {filter: toFilterString(innerFilter)})"
></ui-th>
<th></th>
</thead>
<template v-for="(participant, index) in data" :key="index">
@ -83,21 +86,21 @@
<td v-for="(column, columnindex) in activeColumns" :key="column.id">
<div class="flex items-center space-x-2">
<button v-if="columnindex === 0 && participant.member_id === null" v-tooltip="`kein Mitglied zugewiesen. Per Klick zuweisen`" @click.prevent="assigning = participant">
<ui-sprite src="warning-triangle" class="text-yellow-400 w-5 h-5" />
<ui-sprite src="warning-triangle" class="text-yellow-400 w-5 h-5"></ui-sprite>
</button>
<ui-table-toggle-button v-if="columnindex === 0 && groupParticipants" :value="participant" :level="0" :active="isOpen(participant.id)" @toggle="toggle(participant)">
<prevention v-if="column.display_attribute === 'prevention_display'" :value="participant.prevention_items" />
<span v-else v-text="participant[column.display_attribute]" />
<prevention v-if="column.display_attribute === 'prevention_display'" :value="participant.prevention_items"></prevention>
<span v-else v-text="participant[column.display_attribute]"></span>
</ui-table-toggle-button>
<div v-else>
<prevention v-if="column.display_attribute === 'prevention_display'" :value="participant.prevention_items" />
<span v-else v-text="participant[column.display_attribute]" />
<prevention v-if="column.display_attribute === 'prevention_display'" :value="participant.prevention_items"></prevention>
<span v-else v-text="participant[column.display_attribute]"></span>
</div>
</div>
</td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="editReal(participant)"><ui-sprite src="pencil" /></a>
<a v-tooltip="`Abmelden`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = {model: participant, force: false}"><ui-sprite src="trash" /></a>
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="editReal(participant)"><ui-sprite src="pencil"></ui-sprite></a>
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = participant"><ui-sprite src="trash"></ui-sprite></a>
</td>
</tr>
<template v-for="child in childrenOf(participant.id)" :key="child.id">
@ -105,25 +108,25 @@
<td v-for="(column, columnindex) in activeColumns" :key="column.id">
<div class="flex items-center space-x-2">
<ui-table-toggle-button v-if="columnindex === 0 && groupParticipants" :value="child" :level="1" :active="isOpen(child.id)" @toggle="toggle(child)">
<prevention v-if="column.display_attribute === 'prevention_display'" :value="child.prevention_items" />
<span v-else v-text="child[column.display_attribute]" />
<prevention v-if="column.display_attribute === 'prevention_display'" :value="child.prevention_items"></prevention>
<span v-else v-text="child[column.display_attribute]"></span>
</ui-table-toggle-button>
<div v-else>
<prevention v-if="column.display_attribute === 'prevention_display'" :value="child.prevention_items" />
<span v-else v-text="child[column.display_attribute]" />
<prevention v-if="column.display_attribute === 'prevention_display'" :value="child.prevention_items"></prevention>
<span v-else v-text="child[column.display_attribute]"></span>
</div>
</div>
</td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="editReal(child)"><ui-sprite src="pencil" /></a>
<a v-tooltip="`Abmelden`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = {model: child, force: false}"><ui-sprite src="trash" /></a>
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="editReal(child)"><ui-sprite src="pencil"></ui-sprite></a>
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = child"><ui-sprite src="trash"></ui-sprite></a>
</td>
</tr>
</template>
</template>
</table>
<div class="px-6">
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage($event, {filter: toFilterString(innerFilter)})" />
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage($event, {filter: toFilterString(innerFilter)})"></ui-pagination>
</div>
</div>
</template>
@ -211,7 +214,7 @@ const sortingConfig = computed({
});
async function handleDelete() {
await remove(deleting.value.model, deleting.value.force);
await remove(deleting.value);
deleting.value = null;
}

View File

@ -9,13 +9,15 @@
<div>
<p class="mt-4">Diese Formular-Vorlage löschen?</p>
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#"
class="text-center btn btn-danger"
@click.prevent="
remove(deleting);
deleting = null;
"
>Formular-Vorlage löschen</a>
<a
href="#"
class="text-center btn btn-danger"
@click.prevent="
remove(deleting);
deleting = null;
"
>Formular-Vorlage löschen</a
>
<a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a>
</div>
</div>
@ -23,35 +25,35 @@
<ui-popup v-if="single !== null" :heading="`Vorlage ${single.id ? 'bearbeiten' : 'erstellen'}`" full @close="cancel">
<div class="flex flex-col mt-3">
<ui-tabs v-model="activeTab" :entries="tabs" />
<ui-tabs v-model="activeTab" :entries="tabs"></ui-tabs>
<form-builder v-if="activeTab === 0" v-model="single.config" :meta="meta">
<template #meta>
<f-text id="name" v-model="single.name" label="Name" required />
<f-text id="name" v-model="single.name" label="Name" required></f-text>
</template>
</form-builder>
<div v-show="activeTab === 1" class="grid 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>
Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br>
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 />
Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br />
Außerdem kannst du Dateien hochladen, die automatisch mit angehangen werden.
</ui-note>
<ui-tabs v-model="activeMailTab" :entries="mailTabs" />
<ui-tabs v-model="activeMailTab" :entries="mailTabs"></ui-tabs>
<f-editor v-if="activeMailTab === 0" id="mail_top" v-model="single.mail_top" name="mail_top" label="E-Mail-Teil 1" :rows="8" conditions required>
<template #conditions="{data, resolve}">
<conditions-form id="mail_top_conditions" :single="single" :value="data" @save="resolve" />
<conditions-form id="mail_top_conditions" :single="single" :value="data" @save="resolve"> </conditions-form>
</template>
</f-editor>
<f-editor v-if="activeMailTab === 1" id="mail_bottom" v-model="single.mail_bottom" name="mail_bottom" label="E-Mail-Teil 2" :rows="8" conditions required>
<template #conditions="{data, resolve}">
<conditions-form id="mail_bottom_conditions" :single="single" :value="data" @save="resolve" />
<conditions-form id="mail_bottom_conditions" :single="single" :value="data" @save="resolve"> </conditions-form>
</template>
</f-editor>
</div>
</div>
<template #actions>
<a href="#" @click.prevent="submit">
<ui-sprite src="save" class="text-zinc-400 w-6 h-6" />
<ui-sprite src="save" class="text-zinc-400 w-6 h-6"></ui-sprite>
</a>
</template>
</ui-popup>
@ -59,21 +61,21 @@
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
<thead>
<th>Name</th>
<th />
<th></th>
</thead>
<tr v-for="(formtemplate, index) in data" :key="index">
<td>
<div v-text="formtemplate.name" />
<div v-text="formtemplate.name"></div>
</td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="edit(formtemplate)"><ui-sprite src="pencil" /></a>
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = formtemplate"><ui-sprite src="trash" /></a>
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="edit(formtemplate)"><ui-sprite src="pencil"></ui-sprite></a>
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = formtemplate"><ui-sprite src="trash"></ui-sprite></a>
</td>
</tr>
</table>
<div class="px-6">
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage" />
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage"></ui-pagination>
</div>
</page-layout>
</template>
@ -91,7 +93,7 @@ const tabs = [{ title: 'Formular' }, { title: 'Bestätigungs-E-Mail' }];
const mailTabs = [{ title: 'vor Daten' }, { title: 'nach Daten' }];
const props = defineProps(indexProps);
const { meta, data, reloadPage, create, remove, single, edit, cancel, submit } = useIndex(props.data, 'invoice');
var { meta, data, reloadPage, create, remove, single, edit, cancel, submit } = useIndex(props.data, 'invoice');
function innerSubmit(payload) {
single.value = payload;

View File

@ -1,17 +1,17 @@
<template>
<page-layout>
<template #right>
<f-save-button form="groupform" />
<f-save-button form="groupform"></f-save-button>
</template>
<ui-popup v-if="editing !== null" heading="Untergruppen bearbeiten" inner-width="max-w-5xl" @close="editing = null">
<template #actions>
<a href="#" @click.prevent="store">
<ui-sprite src="save" class="text-zinc-400 w-6 h-6" />
<ui-sprite src="save" class="text-zinc-400 w-6 h-6"></ui-sprite>
</a>
</template>
<div class="flex space-x-3">
<f-text id="parent-inner_name" v-model="editing.parent.inner_name" label="Interner Name" />
<f-select id="parent-level" v-model="editing.parent.level" label="Ebene" name="parent-level" :options="meta.levels" />
<f-text id="parent-inner_name" v-model="editing.parent.inner_name" label="Interner Name"></f-text>
<f-select id="parent-level" v-model="editing.parent.level" label="Ebene" name="parent-level" :options="meta.levels"></f-select>
</div>
<div>
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm table">
@ -23,16 +23,16 @@
</thead>
<tr v-for="child in editing.children" :key="child.id">
<td>
<span v-text="child.name" />
<span v-text="child.name"></span>
</td>
<td>
<f-text :id="`inner_name-${child.id}`" v-model="child.inner_name" label="" size="sm" />
<f-text :id="`inner_name-${child.id}`" v-model="child.inner_name" label="" size="sm"></f-text>
</td>
<td>
<f-select :id="`level-${child.id}`" v-model="child.level" label="" size="sm" :name="`level-${child.id}`" :options="meta.levels" />
<f-select :id="`level-${child.id}`" v-model="child.level" label="" size="sm" :name="`level-${child.id}`" :options="meta.levels"></f-select>
</td>
<td>
<ui-remote-resource :id="`fileshare-${child.id}`" v-model="child.fileshare" size="sm" label="" />
<ui-remote-resource :id="`fileshare-${child.id}`" v-model="child.fileshare" size="sm" label=""></ui-remote-resource>
</td>
</tr>
</table>
@ -44,40 +44,44 @@
<th>NaMi-Name</th>
<th>Interner Name</th>
<th>Ebene</th>
<th />
<th></th>
</thead>
<template v-for="child in childrenOf('null')" :key="child.id">
<tr>
<td>
<ui-table-toggle-button :value="child" :text="child.name" :level="0" :active="isOpen(child.id)" @toggle="toggle(child)" />
<ui-table-toggle-button :value="child" :text="child.name" :level="0" :active="isOpen(child.id)" @toggle="toggle(child)"></ui-table-toggle-button>
</td>
<td v-text="child.inner_name" />
<td v-text="child.level" />
<td v-text="child.inner_name"></td>
<td v-text="child.level"></td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(child)"><ui-sprite src="pencil" /></a>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(child)"><ui-sprite src="pencil"></ui-sprite></a>
</td>
</tr>
<template v-for="subchild in childrenOf(child.id)" :key="subchild.id">
<tr>
<td>
<ui-table-toggle-button :value="subchild" :text="subchild.name" :level="1" :active="isOpen(subchild.id)" @toggle="toggle(subchild)" />
<ui-table-toggle-button :value="subchild" :text="subchild.name" :level="1" :active="isOpen(subchild.id)" @toggle="toggle(subchild)"></ui-table-toggle-button>
</td>
<td v-text="subchild.inner_name" />
<td v-text="subchild.level" />
<td v-text="subchild.inner_name"></td>
<td v-text="subchild.level"></td>
<td>
<a v-if="subchild.children_count" v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(subchild)"><ui-sprite src="pencil" /></a>
<a v-if="subchild.children_count" v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(subchild)"
><ui-sprite src="pencil"></ui-sprite
></a>
</td>
</tr>
<template v-for="subsubchild in childrenOf(subchild.id)" :key="subchild.id">
<tr>
<td>
<ui-table-toggle-button :value="subsubchild" :text="subsubchild.name" :level="2" :active="isOpen(subsubchild.id)" />
<ui-table-toggle-button :value="subsubchild" :text="subsubchild.name" :level="2" :active="isOpen(subsubchild.id)"></ui-table-toggle-button>
</td>
<td v-text="subsubchild.inner_name" />
<td v-text="subsubchild.level" />
<td v-text="subsubchild.inner_name"></td>
<td v-text="subsubchild.level"></td>
<td>
<a v-if="subsubchild.children_count" v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(subsubchild)"><ui-sprite src="pencil" /></a>
<a v-if="subsubchild.children_count" v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(subsubchild)"
><ui-sprite src="pencil"></ui-sprite
></a>
</td>
</tr>
</template>
@ -94,10 +98,10 @@ import { indexProps, useIndex } from '../../composables/useInertiaApiIndex.js';
import useTableToggle from '../../composables/useTableToggle.js';
const props = defineProps(indexProps);
const { axios, meta, data } = useIndex(props.data, 'invoice');
var { axios, meta, data } = useIndex(props.data, 'invoice');
const { isOpen, toggle, childrenOf } = useTableToggle({ null: data.value });
const editing = ref(null);
var editing = ref(null);
async function edit(parent) {
editing.value = {

View File

@ -9,7 +9,7 @@
<ui-popup v-if="massstore !== null" heading="Massenrechnung anlegen" @close="massstore = null">
<form @submit.prevent="sendMassstore">
<section class="grid grid-cols-2 gap-3 mt-6">
<f-text id="year" v-model="massstore.year" label="Jahr" required />
<f-text id="year" v-model="massstore.year" label="Jahr" required></f-text>
</section>
<section class="flex mt-4 space-x-2">
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
@ -21,13 +21,15 @@
<div>
<p class="mt-4">Diese Rechnung löschen?</p>
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#"
class="text-center btn btn-danger"
@click.prevent="
remove(deleting);
deleting = null;
"
>Rechnung löschen</a>
<a
href="#"
class="text-center btn btn-danger"
@click.prevent="
remove(deleting);
deleting = null;
"
>Rechnung löschen</a
>
<a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a>
</div>
</div>
@ -35,33 +37,33 @@
<ui-popup v-if="single !== null" :heading="`Rechnung ${single.id ? 'bearbeiten' : 'erstellen'}`" inner-width="max-w-4xl" @close="cancel">
<form class="grid grid-cols-2 gap-3 mt-4" @submit.prevent="submit">
<ui-box heading="Für Mitglied anlegen" container-class="flex space-x-3" class="col-span-full">
<f-select id="forMemberMember" v-model="forMember.member_id" name="forMemberMember" :options="meta.members" label="Mitglied" />
<f-select id="forMemberSubscription" v-model="forMember.subscription_id" name="forMemberSubscription" :options="meta.subscriptions" label="Beitrag" />
<f-text id="forMemberYear" v-model="forMember.year" label="Jahr" />
<f-select id="forMemberMember" v-model="forMember.member_id" name="forMemberMember" :options="meta.members" label="Mitglied"></f-select>
<f-select id="forMemberSubscription" v-model="forMember.subscription_id" name="forMemberSubscription" :options="meta.subscriptions" label="Beitrag"></f-select>
<f-text id="forMemberYear" v-model="forMember.year" label="Jahr"></f-text>
<ui-icon-button class="btn-primary self-end mb-2" icon="save" @click="saveForMember">Speichern</ui-icon-button>
</ui-box>
<ui-box heading=" Empfänger" container-class="grid grid-cols-2 gap-3 col-span-full">
<f-text id="to_name" v-model="single.to.name" label="Name" class="col-span-full" required />
<f-text id="to_address" v-model="single.to.address" label="Adresse" class="col-span-full" required />
<f-text id="to_zip" v-model="single.to.zip" label="PLZ" required />
<f-text id="to_location" v-model="single.to.location" label="Ort" required />
<f-text id="mail_email" v-model="single.mail_email" label="E-Mail-Adresse" class="col-span-full" />
<f-text id="to_name" v-model="single.to.name" label="Name" class="col-span-full" required></f-text>
<f-text id="to_address" v-model="single.to.address" label="Adresse" class="col-span-full" required></f-text>
<f-text id="to_zip" v-model="single.to.zip" label="PLZ" required></f-text>
<f-text id="to_location" v-model="single.to.location" label="Ort" required></f-text>
<f-text id="mail_email" v-model="single.mail_email" label="E-Mail-Adresse" class="col-span-full"></f-text>
</ui-box>
<ui-box heading="Status" container-class="grid gap-3">
<f-select id="status" v-model="single.status" :options="meta.statuses" name="status" label="Status" required />
<f-select id="via" v-model="single.via" :options="meta.vias" name="via" label="Rechnungsweg" required />
<f-text id="greeting" v-model="single.greeting" label="Anrede" required />
<f-text id="usage" v-model="single.usage" label="Verwendungszweck" required />
<f-select id="status" v-model="single.status" :options="meta.statuses" name="status" label="Status" required></f-select>
<f-select id="via" v-model="single.via" :options="meta.vias" name="via" label="Rechnungsweg" required></f-select>
<f-text id="greeting" v-model="single.greeting" label="Anrede" required></f-text>
<f-text id="usage" v-model="single.usage" label="Verwendungszweck" required></f-text>
</ui-box>
<ui-box heading="Positionen" class="col-span-full" container-class="grid gap-3">
<template #in-title>
<ui-icon-button class="ml-3 btn-primary" icon="plus" @click="single.positions.push({...meta.default_position})">Neu</ui-icon-button>
</template>
<div v-for="(position, index) in single.positions" :key="index" class="flex items-end space-x-3">
<f-text :id="`position-description-${index}`" v-model="position.description" class="grow" label="Beschreibung" required />
<f-text :id="`position-price-${index}`" v-model="position.price" mode="area" label="Preis" required />
<f-select :id="`position-member-${index}`" v-model="position.member_id" :options="meta.members" :name="`position-member-${index}`" label="Mitglied" required />
<button type="button" class="btn btn-danger btn-sm h-[35px]" icon="trash" @click="single.positions.splice(index, 1)"><ui-sprite src="trash" /></button>
<f-text :id="`position-description-${index}`" v-model="position.description" class="grow" label="Beschreibung" required></f-text>
<f-text :id="`position-price-${index}`" v-model="position.price" mode="area" label="Preis" required></f-text>
<f-select :id="`position-member-${index}`" v-model="position.member_id" :options="meta.members" :name="`position-member-${index}`" label="Mitglied" required></f-select>
<button type="button" class="btn btn-danger btn-sm h-[35px]" icon="trash" @click="single.positions.splice(index, 1)"><ui-sprite src="trash"></ui-sprite></button>
</div>
</ui-box>
<section class="flex mt-4 space-x-2">
@ -72,14 +74,15 @@
</ui-popup>
<page-filter>
<template #buttons>
<f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)" />
<f-multipleselect id="statuses"
:options="meta.statuses"
:model-value="getFilter('statuses')"
label="Status"
size="sm"
@update:model-value="setFilter('statuses', $event)"
/>
<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"
:model-value="getFilter('statuses')"
label="Status"
size="sm"
@update:model-value="setFilter('statuses', $event)"
></f-multipleselect>
</template>
</page-filter>
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
@ -89,38 +92,38 @@
<th>Status</th>
<th>Gesendet am</th>
<th>Rechnungsweg</th>
<th />
<th></th>
</thead>
<tr v-for="(invoice, index) in data" :key="index">
<td>
<div v-text="invoice.to.name" />
<div v-text="invoice.to.name"></div>
</td>
<td>
<div v-text="invoice.sum_human" />
<div v-text="invoice.sum_human"></div>
</td>
<td>
<div v-text="invoice.status" />
<div v-text="invoice.status"></div>
</td>
<td>
<div v-text="invoice.sent_at_human" />
<div v-text="invoice.sent_at_human"></div>
</td>
<td>
<div v-text="invoice.via" />
<div v-text="invoice.via"></div>
</td>
<td>
<div class="flex space-x-2">
<ui-action-button tooltip="Anschauen" :href="invoice.links.pdf" class="btn-info" icon="eye" blank />
<ui-action-button tooltip="Erinnerung anschauen" :href="invoice.links.rememberpdf" class="btn-info" icon="document" blank />
<ui-action-button tooltip="Als Bezahlt markieren" class="btn-warning" icon="money" blank @click.prevent="markAsPaid(invoice)" />
<ui-action-button :data-cy="`edit-button-${invoice.id}`" tooltip="Bearbeiten" class="btn-warning" icon="pencil" @click.prevent="edit(invoice)" />
<ui-action-button tooltip="Löschen" class="btn-danger" icon="trash" @click.prevent="deleting = invoice" />
<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>
</td>
</tr>
</table>
<div class="px-6">
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage" />
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage"></ui-pagination>
</div>
</page-layout>
</template>
@ -129,7 +132,7 @@
import { ref } from 'vue';
import { indexProps, useIndex } from '../../composables/useInertiaApiIndex.js';
const props = defineProps(indexProps);
const { axios, meta, data, reloadPage, create, single, edit, cancel, submit, remove, getFilter, setFilter } = useIndex(props.data, 'invoice');
var { axios, meta, data, reloadPage, create, single, edit, cancel, submit, remove, getFilter, setFilter } = useIndex(props.data, 'invoice');
const massstore = ref(null);
const deleting = ref(null);
const forMember = ref({ member_id: null, subscription_id: null, year: null });

View File

@ -3,12 +3,12 @@
<template #toolbar>
<page-toolbar-button :href="data.meta.links.create" color="primary" icon="plus">Verteiler erstellen</page-toolbar-button>
</template>
<ui-popup v-if="deleting !== null" heading="Verteiler löschen?" @close="deleting.reject()">
<ui-popup heading="Verteiler löschen?" v-if="deleting !== null" @close="deleting.reject()">
<div>
<p class="mt-4">Den Verteiler "{{ deleting.dispatcher.name }}" löschen?</p>
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" class="text-center btn btn-danger" @click.prevent="deleting.resolve()">Löschen</a>
<a href="#" class="text-center btn btn-primary" @click.prevent="deleting.reject()">Abbrechen</a>
<a href="#" @click.prevent="deleting.resolve()" class="text-center btn btn-danger">Löschen</a>
<a href="#" @click.prevent="deleting.reject()" class="text-center btn btn-primary">Abbrechen</a>
</div>
</div>
</ui-popup>
@ -17,22 +17,22 @@
<th>Name</th>
<th>Domain</th>
<th>Verbindung</th>
<th />
<th></th>
</thead>
<tr v-for="(dispatcher, index) in data.data" :key="index">
<td>
<div v-text="dispatcher.name" />
<div v-text="dispatcher.name"></div>
</td>
<td>
<div v-text="dispatcher.gateway.domain" />
<div v-text="dispatcher.gateway.domain"></div>
</td>
<td>
<div v-text="dispatcher.gateway.name" />
<div v-text="dispatcher.gateway.name"></div>
</td>
<td>
<i-link :href="dispatcher.links.edit" class="mr-1 inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil" /></i-link>
<button class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(dispatcher)"><ui-sprite src="trash" /></button>
<i-link :href="dispatcher.links.edit" class="mr-1 inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></i-link>
<button @click.prevent="remove(dispatcher)" class="inline-flex btn btn-danger btn-sm"><ui-sprite src="trash"></ui-sprite></button>
</td>
</tr>
</table>
@ -41,9 +41,6 @@
<script>
export default {
props: {
data: {},
},
data: function () {
return {
deleting: null,
@ -63,5 +60,8 @@ export default {
});
},
},
props: {
data: {},
},
};
</script>

View File

@ -5,7 +5,7 @@
<page-toolbar-button v-if="mode === 'edit'" :href="data.links.show" color="primary" icon="eye">anschauen</page-toolbar-button>
</template>
<template #right>
<f-save-button form="memberedit" />
<f-save-button form="memberedit"></f-save-button>
</template>
<form id="memberedit" class="flex grow relative" @submit.prevent="submit">
<ui-popup v-if="conflict === true" heading="Ein Konflikt ist aufgetreten">
@ -24,104 +24,107 @@
<ui-box heading="Stammdaten">
<div class="grid sm:grid-cols-2 gap-3">
<div class="grid grid-cols-2 gap-3">
<f-select id="gender_id" v-model="inner.gender_id" name="gender_id" :options="meta.genders" label="Geschlecht" size="sm" />
<f-text id="salutation" v-model="inner.salutation" size="sm" label="Anrede" />
<f-select id="gender_id" v-model="inner.gender_id" name="gender_id" :options="meta.genders" label="Geschlecht" size="sm"></f-select>
<f-text id="salutation" v-model="inner.salutation" size="sm" label="Anrede"></f-text>
</div>
<f-select id="nationality_id" v-model="inner.nationality_id" :options="meta.nationalities" label="Staatsangehörigkeit" name="nationality_id" size="sm" />
<f-text id="firstname" v-model="inner.firstname" size="sm" label="Vorname" required />
<f-text id="lastname" v-model="inner.lastname" size="sm" label="Nachname" required />
<f-text id="address" v-model="inner.address" size="sm" label="Adresse" />
<f-text id="further_address" v-model="inner.further_address" size="sm" label="Adresszusatz" />
<f-text id="zip" v-model="inner.zip" size="sm" label="PLZ" />
<f-text id="location" v-model="inner.location" size="sm" label="Ort" />
<f-text id="birthday" v-model="inner.birthday" type="date" size="sm" label="Geburtsdatum" />
<f-select id="region_id" v-model="inner.region_id" :options="meta.regions" name="region_id" label="Bundesland" size="sm" />
<f-select id="country_id" v-model="inner.country_id" :options="meta.countries" label="Land" name="country_id" size="sm" required />
<f-text id="other_country" v-model="inner.other_country" label="Andere Staatsangehörigkeit" size="sm" />
<f-select id="nationality_id" v-model="inner.nationality_id" :options="meta.nationalities" label="Staatsangehörigkeit" name="nationality_id" size="sm"></f-select>
<f-text id="firstname" v-model="inner.firstname" size="sm" label="Vorname" required></f-text>
<f-text id="lastname" v-model="inner.lastname" size="sm" label="Nachname" required></f-text>
<f-text id="address" v-model="inner.address" size="sm" label="Adresse"></f-text>
<f-text id="further_address" v-model="inner.further_address" size="sm" label="Adresszusatz"></f-text>
<f-text id="zip" v-model="inner.zip" size="sm" label="PLZ"></f-text>
<f-text id="location" v-model="inner.location" size="sm" label="Ort"></f-text>
<f-text id="birthday" v-model="inner.birthday" type="date" size="sm" label="Geburtsdatum"></f-text>
<f-select id="region_id" v-model="inner.region_id" :options="meta.regions" name="region_id" label="Bundesland" size="sm"></f-select>
<f-select id="country_id" v-model="inner.country_id" :options="meta.countries" label="Land" name="country_id" size="sm" required></f-select>
<f-text id="other_country" v-model="inner.other_country" label="Andere Staatsangehörigkeit" size="sm"></f-text>
</div>
</ui-box>
<ui-box heading="Kontakt">
<div class="grid gap-3 sm:grid-cols-2">
<f-text id="main_phone" v-model="inner.main_phone" size="sm" label="Telefon (Eltern)" />
<f-text id="mobile_phone" v-model="inner.mobile_phone" size="sm" label="Handy (Eltern)" />
<f-text id="work_phone" v-model="inner.work_phone" size="sm" label="Tel geschäftlich (Eltern)" />
<f-text id="children_phone" v-model="inner.children_phone" size="sm" label="Telefon (Kind)" />
<f-text id="email" v-model="inner.email" size="sm" label="E-Mail" />
<f-text id="email_parents" v-model="inner.email_parents" size="sm" label="E-Mail eltern" />
<f-text id="fax" v-model="inner.fax" size="sm" label="Fax" />
<f-textarea id="letter_address" v-model="inner.letter_address" class="sm:col-span-2" :rows="3" label="Brief-Adresse" size="sm" />
<f-text id="main_phone" v-model="inner.main_phone" size="sm" label="Telefon (Eltern)"></f-text>
<f-text id="mobile_phone" v-model="inner.mobile_phone" size="sm" label="Handy (Eltern)"></f-text>
<f-text id="work_phone" v-model="inner.work_phone" size="sm" label="Tel geschäftlich (Eltern)"></f-text>
<f-text id="children_phone" v-model="inner.children_phone" size="sm" label="Telefon (Kind)"></f-text>
<f-text id="email" v-model="inner.email" size="sm" label="E-Mail"></f-text>
<f-text id="email_parents" v-model="inner.email_parents" size="sm" label="E-Mail eltern"></f-text>
<f-text id="fax" v-model="inner.fax" size="sm" label="Fax"></f-text>
<f-textarea id="letter_address" v-model="inner.letter_address" class="sm:col-span-2" :rows="3" label="Brief-Adresse" size="sm"></f-textarea>
</div>
</ui-box>
<ui-box heading="System">
<div class="grid gap-3">
<f-select id="bill_kind" v-model="inner.bill_kind" :options="meta.billKinds" label="Rechnung versenden über" name="bill_kind" size="sm" />
<f-select id="subscription_id" v-model="inner.subscription_id" :options="meta.subscriptions" label="Beitrag" name="subscription_id" size="sm" />
<f-switch id="has_nami" v-model="inner.has_nami" name="has_nami" size="sm" label="In Nami eintragen" />
<f-switch id="send_newspaper" v-model="inner.send_newspaper" name="send_newspaper" label="Mittendrin versenden" size="sm" />
<f-switch id="keepdata" v-model="inner.keepdata" name="keepdata" label="Datenweiterverwendung" size="sm" />
<f-text id="joined_at" v-model="inner.joined_at" type="date" label="Eintrittsdatum" size="sm" required />
<f-textarea id="comment" v-model="inner.comment" :rows="3" class="col-span-2" label="Kommentar" size="sm" />
<f-select id="bill_kind" v-model="inner.bill_kind" :options="meta.billKinds" label="Rechnung versenden über" name="bill_kind" size="sm"></f-select>
<f-select id="subscription_id" v-model="inner.subscription_id" :options="meta.subscriptions" label="Beitrag" name="subscription_id" size="sm"></f-select>
<f-switch id="has_nami" v-model="inner.has_nami" name="has_nami" size="sm" label="In Nami eintragen"></f-switch>
<f-switch id="send_newspaper" v-model="inner.send_newspaper" name="send_newspaper" label="Mittendrin versenden" size="sm"></f-switch>
<f-switch id="keepdata" v-model="inner.keepdata" name="keepdata" label="Datenweiterverwendung" size="sm"></f-switch>
<f-text id="joined_at" v-model="inner.joined_at" type="date" label="Eintrittsdatum" size="sm" required></f-text>
<f-textarea id="comment" v-model="inner.comment" :rows="3" class="col-span-2" label="Kommentar" size="sm"></f-textarea>
<div v-if="mode === 'create' || (original.has_nami === false && inner.has_nami === true)" class="contents">
<f-select id="first_activity_id"
v-model="inner.first_activity_id"
:options="meta.formCreateActivities"
label="Erste Tätigkeit"
name="first_activity_id"
size="sm"
required
/>
<f-select v-if="inner.first_activity_id"
id="first_subactivity_id"
v-model="inner.first_subactivity_id"
:options="meta.formCreateSubactivities[inner.first_activity_id]"
label="Erste Untertätigkeit"
name="first_subactivity_id"
size="sm"
required
/>
<f-select
id="first_activity_id"
v-model="inner.first_activity_id"
:options="meta.formCreateActivities"
label="Erste Tätigkeit"
name="first_activity_id"
size="sm"
required
></f-select>
<f-select
v-if="inner.first_activity_id"
id="first_subactivity_id"
v-model="inner.first_subactivity_id"
:options="meta.formCreateSubactivities[inner.first_activity_id]"
label="Erste Untertätigkeit"
name="first_subactivity_id"
size="sm"
required
></f-select>
</div>
</div>
</ui-box>
<ui-box heading="Prävention">
<div class="grid sm:grid-cols-[minmax(min-content,max-content)_minmax(min-content,max-content)] gap-2">
<div class="grid grid-cols-[minmax(min-content,max-content)_8rem] gap-1">
<f-switch id="has_efz" v-model="hasEfz" name="has_efz" size="sm" label="Führungszeugnis eingesehen" />
<f-switch id="has_efz" v-model="hasEfz" name="has_efz" size="sm" label="Führungszeugnis eingesehen"></f-switch>
<div>
<f-text v-if="inner.efz !== null" id="efz" v-model="inner.efz" type="date" label="am" size="sm" />
<f-text v-if="inner.efz !== null" id="efz" v-model="inner.efz" type="date" label="am" size="sm"></f-text>
</div>
<f-switch id="has_ps" v-model="hasPs" name="has_ps" size="sm" label="Hat Präventionsschulung" />
<f-switch id="has_ps" v-model="hasPs" name="has_ps" size="sm" label="Hat Präventionsschulung"></f-switch>
<div>
<f-text v-if="inner.ps_at !== null" id="ps_at" v-model="inner.ps_at" type="date" label="am" size="sm" />
<f-text v-if="inner.ps_at !== null" id="ps_at" v-model="inner.ps_at" type="date" label="am" size="sm"></f-text>
</div>
<f-switch id="has_more_ps" v-model="hasMorePs" name="has_more_ps" size="sm" label="Hat Vertiefungsschulung" />
<f-switch id="has_more_ps" v-model="hasMorePs" name="has_more_ps" size="sm" label="Hat Vertiefungsschulung"></f-switch>
<div>
<f-text v-if="inner.more_ps_at !== null" id="more_ps_at" v-model="inner.more_ps_at" type="date" label="am" size="sm" />
<f-text v-if="inner.more_ps_at !== null" id="more_ps_at" v-model="inner.more_ps_at" type="date" label="am" size="sm"></f-text>
</div>
<f-switch id="is_recertified" v-model="isRecertified" name="is_recertified" size="sm" label="Hat Rezertifizierung" />
<f-switch id="is_recertified" v-model="isRecertified" name="is_recertified" size="sm" label="Hat Rezertifizierung"></f-switch>
<div>
<f-text v-if="inner.recertified_at !== null" id="recertified_at" v-model="inner.recertified_at" type="date" label="am" size="sm" />
<f-text v-if="inner.recertified_at !== null" id="recertified_at" v-model="inner.recertified_at" type="date" label="am" size="sm"></f-text>
</div>
<f-switch id="has_without_education" v-model="hasWithoutEducation" name="has_without_education" label="Einsatz ohne Schulung" size="sm" />
<f-switch id="has_without_education" v-model="hasWithoutEducation" name="has_without_education" label="Einsatz ohne Schulung" size="sm"></f-switch>
<div>
<f-text v-if="inner.without_education_at !== null"
id="without_education_at"
v-model="inner.without_education_at"
name="without_education_at"
type="date"
label="am"
size="sm"
/>
<f-text
v-if="inner.without_education_at !== null"
id="without_education_at"
v-model="inner.without_education_at"
name="without_education_at"
type="date"
label="am"
size="sm"
></f-text>
</div>
<f-switch id="has_without_efz" v-model="hasWithoutEfz" name="has_without_efz" size="sm" label="Einsatz ohne EFZ" />
<f-switch id="has_without_efz" v-model="hasWithoutEfz" name="has_without_efz" size="sm" label="Einsatz ohne EFZ"></f-switch>
<div>
<f-text v-if="inner.without_efz_at !== null" id="without_efz_at" v-model="inner.without_efz_at" type="date" label="am" size="sm" />
<f-text v-if="inner.without_efz_at !== null" id="without_efz_at" v-model="inner.without_efz_at" type="date" label="am" size="sm"></f-text>
</div>
</div>
<div class="grid gap-1">
<f-switch id="has_svk" v-model="inner.has_svk" name="has_svk" size="sm" label="SVK unterschrieben" />
<f-switch id="has_vk" v-model="inner.has_vk" name="has_vk" size="sm" label="Verhaltenskodex unterschrieben" />
<f-switch id="multiply_pv" v-model="inner.multiply_pv" name="multiply_pv" label="Multiplikator*in Präventionsschulung" size="sm" />
<f-switch id="multiply_more_pv" v-model="inner.multiply_more_pv" name="multiply_more_pv" label="Multiplikator*in Vertiefungsschulung" size="sm" />
<f-switch id="has_svk" v-model="inner.has_svk" name="has_svk" size="sm" label="SVK unterschrieben"></f-switch>
<f-switch id="has_vk" v-model="inner.has_vk" name="has_vk" size="sm" label="Verhaltenskodex unterschrieben"></f-switch>
<f-switch id="multiply_pv" v-model="inner.multiply_pv" name="multiply_pv" label="Multiplikator*in Präventionsschulung" size="sm"></f-switch>
<f-switch id="multiply_more_pv" v-model="inner.multiply_more_pv" name="multiply_more_pv" label="Multiplikator*in Vertiefungsschulung" size="sm"></f-switch>
</div>
</div>
</ui-box>
@ -159,7 +162,7 @@ export default {
methods: {
submit() {
this.mode === 'create' ? this.$inertia.post('/member', this.inner) : this.$inertia.patch(`/member/${this.inner.id}`, this.inner);
this.mode === 'create' ? this.$inertia.post(`/member`, this.inner) : this.$inertia.patch(`/member/${this.inner.id}`, this.inner);
},
resync() {
this.$inertia.get(`/member/${this.inner.id}/resync`);

View File

@ -22,9 +22,9 @@
<member-filter-fields :model-value="filter" @update:model-value="setFilterObject($event)" />
</template>
<template #buttons>
<f-text id="search" :model-value="filter.search" label="Suchen …" size="sm" @update:model-value="setFilterObject({...filter, search: $event})" />
<f-text id="search" :model-value="filter.search" label="Suchen …" size="sm" @update:model-value="setFilterObject({...filter, search: $event})"></f-text>
<button class="btn btn-primary label mr-2" @click.prevent="exportMembers">
<ui-sprite class="w-3 h-3 xl:mr-2" src="save" />
<ui-sprite class="w-3 h-3 xl:mr-2" src="save"></ui-sprite>
<span class="hidden xl:inline">Exportieren</span>
</button>
</template>
@ -32,7 +32,7 @@
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
<thead>
<th />
<th></th>
<th>Nachname</th>
<th>Vorname</th>
<th class="!hidden 2xl:!table-cell">Ort</th>
@ -40,26 +40,26 @@
<th class="!hidden xl:!table-cell">Alter</th>
<th v-if="hasModule('bill')" class="!hidden xl:!table-cell">Rechnung</th>
<th v-if="hasModule('bill')">Ausstand</th>
<th />
<th></th>
</thead>
<tr v-for="(member, index) in data" :key="index">
<td><ui-age-groups :member="member" /></td>
<td v-text="member.lastname" />
<td v-text="member.firstname" />
<td class="!hidden 2xl:!table-cell" v-text="member.full_address" />
<td><ui-age-groups :member="member"></ui-age-groups></td>
<td v-text="member.lastname"></td>
<td v-text="member.firstname"></td>
<td class="!hidden 2xl:!table-cell" v-text="member.full_address"></td>
<td>
<tags :member="member" />
<tags :member="member"></tags>
</td>
<td class="!hidden xl:!table-cell" v-text="member.age" />
<td class="!hidden xl:!table-cell" v-text="member.age"></td>
<td v-if="hasModule('bill')" class="!hidden xl:!table-cell">
<ui-label :value="member.bill_kind_name" fallback="kein" />
<ui-label :value="member.bill_kind_name" fallback="kein"></ui-label>
</td>
<td v-if="hasModule('bill')">
<ui-label :value="member.pending_payment" fallback="---" />
<ui-label :value="member.pending_payment" fallback="---"></ui-label>
</td>
<td>
<actions :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)" />
<actions :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)"></actions>
</td>
</tr>
</table>
@ -67,28 +67,28 @@
<div class="md:hidden p-3 grid gap-3">
<ui-box v-for="(member, index) in data" :key="index" class="relative" :heading="member.fullname">
<template #in-title>
<ui-age-groups class="ml-2" :member="member" icon-class="w-4 h-4" />
<ui-age-groups class="ml-2" :member="member" icon-class="w-4 h-4"></ui-age-groups>
</template>
<div class="text-xs text-gray-200" v-text="member.full_address" />
<div class="text-xs text-gray-200" v-text="member.full_address"></div>
<div class="flex items-center mt-1 space-x-4">
<tags :member="member" />
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" fallback="" />
<tags :member="member"></tags>
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" fallback=""></ui-label>
</div>
<actions class="mt-2" :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)" />
<actions class="mt-2" :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)"> </actions>
<div class="absolute right-0 top-0 h-full flex items-center mr-2">
<i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron" class="w-6 h-6 text-teal-100 -rotate-90" /></i-link>
<i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron" class="w-6 h-6 text-teal-100 -rotate-90"></ui-sprite></i-link>
</div>
</ui-box>
</div>
<div class="px-6">
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage" />
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage"></ui-pagination>
</div>
<ui-sidebar v-if="single !== null" @close="closeSidebar">
<member-invoice-positions v-if="single.type === 'invoicePosition'" :url="single.model.links.invoiceposition_index" @close="closeSidebar" />
<member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index" @close="closeSidebar" />
<member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index" @close="closeSidebar" />
<member-invoice-positions v-if="single.type === 'invoicePosition'" :url="single.model.links.invoiceposition_index" @close="closeSidebar"></member-invoice-positions>
<member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index" @close="closeSidebar"></member-memberships>
<member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index" @close="closeSidebar"></member-courses>
</ui-sidebar>
</page-layout>
</template>
@ -107,7 +107,7 @@ const single = ref(null);
const deleting = ref(null);
const props = defineProps(indexProps);
const {router, data, meta, filter, setFilterObject, filterString, reloadPage} = useIndex(props.data, 'member');
var {router, data, meta, filter, setFilterObject, filterString, reloadPage} = useIndex(props.data, 'member');
function exportMembers() {
window.open(`/member-export?filter=${filterString.value}`);

View File

@ -43,7 +43,6 @@
<th>Beginn</th>
<th>Ende</th>
<th>Aktiv</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
@ -55,9 +54,6 @@
<td v-text="membership.from.human" />
<td v-text="membership.to?.human" />
<td><ui-bool :value="membership.isActive" /></td>
<td>
<ui-action-button tooltip="Löschen" class="btn-danger" icon="trash" @click.prevent="onDelete(membership)" />
</td>
</tr>
</tbody>
</table>
@ -70,14 +66,9 @@
<script lang="js" setup>
import {useIndex, indexProps} from '@/composables/useIndex.js';
import useSwal from '@/stores/swalStore.ts';
const swal = useSwal();
const props = defineProps(indexProps);
const {data, meta, getFilter, setFilter, axios} = useIndex(props.data, 'membership');
async function onDelete(membership) {
await swal.confirm('Mitgliedschaft löschen', `Mitgliedschaft von ${membership.member.fullname} löschen`);
await axios.delete(membership.links.destroy);
}
const {data, meta, getFilter, setFilter} = useIndex(props.data, 'memberships');
</script>

View File

@ -6,31 +6,33 @@
<ui-popup v-if="single !== null" :heading="single.id ? 'Verbindung bearbeiten' : 'Neue Verbindung'" @close="cancel">
<form @submit.prevent="submit">
<section class="grid grid-cols-2 gap-3 mt-6">
<f-text id="name" v-model="single.name" name="name" label="Bezeichnung" required />
<f-select id="type"
:model-value="single.type"
label="Typ"
name="type"
:options="meta.types"
required
@update:model-value="
single = {
...single,
type: $event,
config: {...getType($event).defaults},
}
"
/>
<f-text id="name" v-model="single.name" name="name" label="Bezeichnung" required></f-text>
<f-select
id="type"
:model-value="single.type"
label="Typ"
name="type"
:options="meta.types"
required
@update:model-value="
single = {
...single,
type: $event,
config: {...getType($event).defaults},
}
"
></f-select>
<template v-for="(field, index) in getType(single.type).fields">
<f-text v-if="field.type === 'text' || field.type === 'password' || field.type === 'email'"
:id="field.key"
:key="index"
v-model="single.config[field.key]"
:label="field.label"
:type="field.type"
:name="field.key"
required
/>
<f-text
v-if="field.type === 'text' || field.type === 'password' || field.type === 'email'"
:id="field.key"
:key="index"
v-model="single.config[field.key]"
:label="field.label"
:type="field.type"
:name="field.key"
required
></f-text>
</template>
</section>
<section class="flex mt-4 space-x-2">
@ -50,22 +52,23 @@
</thead>
<tr v-for="(connection, index) in data" :key="index">
<td v-text="connection.name" />
<td v-text="connection.type_human" />
<td v-text="connection.name"></td>
<td v-text="connection.type_human"></td>
<td>
<ui-boolean-display :value="connection.is_active"
long-label="Verbindungsstatus"
:label="connection.is_active ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'"
/>
<ui-boolean-display
:value="connection.is_active"
long-label="Verbindungsstatus"
:label="connection.is_active ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'"
></ui-boolean-display>
</td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(connection)"><ui-sprite src="pencil" /></a>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(connection)"><ui-sprite src="pencil"></ui-sprite></a>
</td>
</tr>
</table>
<div class="px-6">
<ui-pagination class="mt-4" :value="meta" :only="['data']" />
<ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div>
</div>
</setting-layout>

View File

@ -11,7 +11,6 @@
<div class="grid grid-cols-2 gap-4">
<f-text id="register_url" v-model="inner.registerUrl" label="Formular-Link"></f-text>
<f-text id="clear_cache_url" v-model="inner.clearCacheUrl" label="Frontend-Cache-Url"></f-text>
<f-text id="reply_to_mail" v-model="inner.replyToMail" label="Reply-To-Adresse"></f-text>
</div>
</form>
</setting-layout>

View File

@ -6,31 +6,33 @@
<ui-popup v-if="single !== null" :heading="single.id ? 'Verbindung bearbeiten' : 'Neue Verbindung'" @close="cancel">
<form @submit.prevent="submit">
<section class="grid grid-cols-2 gap-3 mt-6">
<f-text id="name" v-model="single.name" label="Bezeichnung" required />
<f-text id="domain" v-model="single.domain" label="Domain" required />
<f-select id="type"
:model-value="single.type.cls"
label="Typ"
name="type"
:options="meta.types"
:placeholder="''"
required
@update:model-value="
single.type = {
cls: $event,
params: {...getType($event).defaults},
}
"
/>
<f-text id="name" v-model="single.name" label="Bezeichnung" required></f-text>
<f-text id="domain" v-model="single.domain" label="Domain" required></f-text>
<f-select
id="type"
:model-value="single.type.cls"
label="Typ"
name="type"
:options="meta.types"
:placeholder="''"
required
@update:model-value="
single.type = {
cls: $event,
params: {...getType($event).defaults},
}
"
></f-select>
<template v-for="(field, index) in getType(single.type.cls).fields">
<f-text v-if="field.type === 'text' || field.type === 'password' || field.type === 'email'"
:id="field.name"
:key="index"
v-model="single.type.params[field.name]"
:label="field.label"
:type="field.type"
:required="field.is_required"
/>
<f-text
v-if="field.type === 'text' || field.type === 'password' || field.type === 'email'"
:id="field.name"
:key="index"
v-model="single.type.params[field.name]"
:label="field.label"
:type="field.type"
:required="field.is_required"
></f-text>
</template>
</section>
<section class="flex mt-4 space-x-2">
@ -51,23 +53,24 @@
</thead>
<tr v-for="(gateway, index) in data" :key="index">
<td v-text="gateway.name" />
<td v-text="gateway.domain" />
<td v-text="gateway.type_human" />
<td v-text="gateway.name"></td>
<td v-text="gateway.domain"></td>
<td v-text="gateway.type_human"></td>
<td>
<ui-boolean-display :value="gateway.works"
long-label="Verbindungsstatus"
:label="gateway.works ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'"
/>
<ui-boolean-display
:value="gateway.works"
long-label="Verbindungsstatus"
:label="gateway.works ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'"
></ui-boolean-display>
</td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(gateway)"><ui-sprite src="pencil" /></a>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(gateway)"><ui-sprite src="pencil"></ui-sprite></a>
</td>
</tr>
</table>
<div class="px-6">
<ui-pagination class="mt-4" :value="meta" :only="['data']" />
<ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div>
</div>
</setting-layout>

View File

@ -22,7 +22,6 @@
<f-editor v-if="active === 1" id="yearlymail" v-model="data.yearlymail" label="Jährliche Präventions-Erinnerung"></f-editor>
<f-member-filter id="yearly_member_filter" v-model="data.yearlyMemberFilter" label="nur für folgende Mitglieder erlauben" />
<f-multipleselect id="prevent_against" v-model="data.preventAgainst" :options="meta.preventAgainsts" label="An diese Dokumente erinnern" size="sm"></f-multipleselect>
<f-text id="reply_to_mail" v-model="data.replyToMail" label="Reply-To-Adresse"></f-text>
</div>
</form>
</setting-layout>

View File

@ -11,6 +11,7 @@ use App\Activity\Api\SubactivityStoreAction;
use App\Activity\Api\SubactivityUpdateAction;
use App\Contribution\Actions\FormAction as ContributionFormAction;
use App\Contribution\Actions\GenerateAction as ContributionGenerateAction;
use App\Contribution\Actions\ValidateAction as ContributionValidateAction;
use App\Course\Actions\CourseDestroyAction;
use App\Course\Actions\CourseIndexAction;
use App\Course\Actions\CourseStoreAction;
@ -22,9 +23,7 @@ use App\Fileshare\Actions\FileshareStoreAction;
use App\Fileshare\Actions\FileshareUpdateAction;
use App\Fileshare\Actions\ListFilesAction;
use App\Form\Actions\ExportAction as ActionsExportAction;
use App\Form\Actions\FormCopyAction;
use App\Form\Actions\FormDestroyAction;
use App\Form\Actions\FormGenerateLaterlinkAction;
use App\Form\Actions\FormIndexAction;
use App\Group\Actions\GroupBulkstoreAction;
use App\Group\Actions\GroupIndexAction;
@ -35,7 +34,6 @@ use App\Form\Actions\FormtemplateStoreAction;
use App\Form\Actions\FormtemplateUpdateAction;
use App\Form\Actions\FormUpdateAction;
use App\Form\Actions\FormUpdateMetaAction;
use App\Form\Actions\GenerateContributionAction;
use App\Form\Actions\IsDirtyAction;
use App\Form\Actions\ParticipantAssignAction;
use App\Form\Actions\ParticipantDestroyAction;
@ -71,6 +69,7 @@ use App\Member\Actions\MemberResyncAction;
use App\Member\Actions\MemberShowAction;
use App\Member\Actions\SearchAction;
use App\Member\MemberController;
use App\Membership\Actions\IndexAction as MembershipIndexAction;
use App\Membership\Actions\ListForGroupAction;
use App\Membership\Actions\MassListAction;
use App\Membership\Actions\MassStoreAction;
@ -112,6 +111,7 @@ Route::group(['middleware' => 'auth:web'], function (): void {
// ------------------------------- Contributions -------------------------------
Route::get('/contribution', ContributionFormAction::class)->name('contribution.form');
Route::get('/contribution-generate', ContributionGenerateAction::class)->name('contribution.generate');
Route::post('/contribution-validate', ContributionValidateAction::class)->name('contribution.validate');
// ----------------------------------- mail ------------------------------------
Route::post('/api/mailgateway', StoreAction::class)->name('mailgateway.store');
@ -179,9 +179,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::get('/participant/{participant}/fields', ParticipantFieldsAction::class)->name('participant.fields');
Route::patch('/participant/{participant}', ParticipantUpdateAction::class)->name('participant.update');
Route::post('/form/{form}/participant', ParticipantStoreAction::class)->name('form.participant.store');
Route::post('/form/{form}/copy', FormCopyAction::class)->name('form.copy');
Route::get('/form/{form}/contribution', GenerateContributionAction::class)->name('form.contribution');
Route::get('/form/{form}/laterlink', FormGenerateLaterlinkAction::class)->name('form.laterlink');
// ------------------------------------ fileshare -----------------------------------
Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store');

Some files were not shown because too many files have changed in this diff Show More