Compare commits
No commits in common. "ebf856252c12829614df25dc6e15e99b8afa193e" and "20836e7228cd22430409e57c0f66f69a2835783f" have entirely different histories.
ebf856252c
...
20836e7228
|
@ -4,10 +4,10 @@ namespace App\Dashboard;
|
|||
|
||||
use App\Dashboard\Blocks\Block;
|
||||
use App\Efz\EfzPendingBlock;
|
||||
use App\Invoice\MemberPaymentBlock;
|
||||
use App\Member\PsPendingBlock;
|
||||
use App\Membership\AgeGroupCountBlock;
|
||||
use App\Membership\TestersBlock;
|
||||
use App\Payment\MemberPaymentBlock;
|
||||
|
||||
class DashboardFactory
|
||||
{
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use App\Invoice\BillDocument;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use Illuminate\Http\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\Tex\BaseCompiler;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class DisplayPdfAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Invoice $invoice): BaseCompiler|Response
|
||||
{
|
||||
return Tex::compile(BillDocument::fromInvoice($invoice));
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\RememberDocument;
|
||||
use Illuminate\Http\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\Tex\BaseCompiler;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class DisplayRememberpdfAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Invoice $invoice): BaseCompiler|Response
|
||||
{
|
||||
return Tex::compile(RememberDocument::fromInvoice($invoice));
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
trait HasValidation
|
||||
{
|
||||
/**
|
||||
* @return array<string, string|array<int, string|Rule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['required', 'string', 'max:255', Rule::in(InvoiceStatus::values())],
|
||||
'via' => ['required', 'string', 'max:255', Rule::in(BillKind::values())],
|
||||
'usage' => 'required|max:255|string',
|
||||
'mail_email' => 'nullable|string|max:255|email',
|
||||
'to' => 'array',
|
||||
'to.address' => 'required|string|max:255',
|
||||
'to.location' => 'required|string|max:255',
|
||||
'to.zip' => 'required|string|max:255',
|
||||
'to.name' => 'required|string|max:255',
|
||||
'greeting' => 'required|string|max:255',
|
||||
'positions' => 'array',
|
||||
'positions.*.description' => 'required|string|max:300',
|
||||
'positions.*.price' => 'required|integer|min:0',
|
||||
'positions.*.member_id' => 'required|exists:members,id',
|
||||
'positions.*.id' => 'present|nullable|exists:invoice_positions,id',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'to.address' => 'Adresse',
|
||||
'to.name' => 'Name',
|
||||
'to.zip' => 'PLZ',
|
||||
'to.location' => 'Ort',
|
||||
'status' => 'Status',
|
||||
'via' => 'Rechnungsweg',
|
||||
'usage' => 'Verwendungszweck',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class InvoiceDestroyAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Invoice $invoice): JsonResponse
|
||||
{
|
||||
$invoice->delete();
|
||||
|
||||
Succeeded::message('Rechnung gelöscht.')->dispatch();
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\Resources\InvoiceResource;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class InvoiceIndexAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<Invoice>
|
||||
*/
|
||||
public function handle(): LengthAwarePaginator
|
||||
{
|
||||
return Invoice::select('*')->with('positions')->paginate(15);
|
||||
}
|
||||
|
||||
public function asController(): Response
|
||||
{
|
||||
session()->put('menu', 'invoice');
|
||||
session()->put('title', 'Rechnungen');
|
||||
|
||||
return Inertia::render('invoice/Index', [
|
||||
'data' => InvoiceResource::collection($this->handle()),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -2,13 +2,10 @@
|
|||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use App\Invoice\BillDocument;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\Mails\BillMail;
|
||||
use App\Invoice\Mails\RememberMail;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\RememberDocument;
|
||||
use App\Invoice\DocumentFactory;
|
||||
use App\Invoice\Queries\BillKindQuery;
|
||||
use App\Payment\PaymentMail;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
@ -35,18 +32,15 @@ class InvoiceSendAction
|
|||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
foreach (Invoice::whereNeedsBill()->where('via', BillKind::EMAIL)->get() as $invoice) {
|
||||
$document = BillDocument::fromInvoice($invoice);
|
||||
$path = Storage::disk('temp')->path(Tex::compile($document)->storeIn('', 'temp'));
|
||||
Mail::to($invoice->getMailRecipient())->send(new BillMail($invoice, $path));
|
||||
$invoice->sent($document);
|
||||
}
|
||||
foreach (app(DocumentFactory::class)->getTypes() as $type) {
|
||||
$invoices = app(DocumentFactory::class)->invoiceCollection($type, new BillKindQuery(BillKind::EMAIL));
|
||||
|
||||
foreach (Invoice::whereNeedsRemember()->where('via', BillKind::EMAIL)->get() as $invoice) {
|
||||
$document = RememberDocument::fromInvoice($invoice);
|
||||
$path = Storage::disk('temp')->path(Tex::compile($document)->storeIn('', 'temp'));
|
||||
Mail::to($invoice->getMailRecipient())->send(new RememberMail($invoice, $path));
|
||||
$invoice->sent($document);
|
||||
foreach ($invoices as $invoice) {
|
||||
$invoicePath = Storage::disk('temp')->path(Tex::compile($invoice)->storeIn('', 'temp'));
|
||||
Mail::to($invoice->getRecipient())
|
||||
->send(new PaymentMail($invoice, $invoicePath));
|
||||
app(DocumentFactory::class)->afterSingle($invoice);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Lib\Events\Succeeded;
|
||||
|
||||
class InvoiceStoreAction
|
||||
{
|
||||
use AsAction;
|
||||
use HasValidation;
|
||||
|
||||
public function handle(ActionRequest $request): void
|
||||
{
|
||||
$invoice = Invoice::create($request->safe()->except('positions'));
|
||||
|
||||
foreach ($request->validated('positions') as $position) {
|
||||
$invoice->positions()->create($position);
|
||||
}
|
||||
|
||||
Succeeded::message('Rechnung erstellt.')->dispatch();
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class InvoiceUpdateAction
|
||||
{
|
||||
use AsAction;
|
||||
use HasValidation;
|
||||
|
||||
public function handle(Invoice $invoice, ActionRequest $request): void
|
||||
{
|
||||
$invoice->update($request->safe()->except('positions'));
|
||||
|
||||
foreach ($request->validated('positions') as $position) {
|
||||
if ($position['id']) {
|
||||
$invoice->positions()->firstWhere('id', $position['id'])->update(Arr::except($position, 'id'));
|
||||
continue;
|
||||
}
|
||||
|
||||
$invoice->positions()->create($position);
|
||||
}
|
||||
|
||||
$invoice->positions()->whereNotIn('id', array_column($request->validated('positions'), 'id'))->delete();
|
||||
|
||||
Succeeded::message('Rechnung bearbeitet.')->dispatch();
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use App\Invoice\BillDocument;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\RememberDocument;
|
||||
use Illuminate\Http\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\Tex\BaseCompiler;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class MassPostPdfAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(): BaseCompiler|Response
|
||||
{
|
||||
$documents = [];
|
||||
|
||||
foreach (Invoice::whereNeedsBill()->where('via', BillKind::POST)->get() as $invoice) {
|
||||
$document = BillDocument::fromInvoice($invoice);
|
||||
$documents[] = $document;
|
||||
$invoice->sent($document);
|
||||
}
|
||||
|
||||
foreach (Invoice::whereNeedsRemember()->where('via', BillKind::POST)->get() as $invoice) {
|
||||
$document = RememberDocument::fromInvoice($invoice);
|
||||
$documents[] = $document;
|
||||
$invoice->sent($document);
|
||||
}
|
||||
|
||||
if (!count($documents)) {
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
return Tex::merge($documents);
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use App\Invoice\Events\InvoicesMassStored;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Lib\JobMiddleware\JobChannels;
|
||||
use App\Lib\JobMiddleware\WithJobState;
|
||||
use App\Lib\Queue\TracksJob;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class MassStoreAction
|
||||
{
|
||||
use AsAction;
|
||||
use TracksJob;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => 'required|numeric',
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(int $year): void
|
||||
{
|
||||
/** @var Collection<int, Invoice> */
|
||||
$invoices = collect([]);
|
||||
|
||||
$memberGroup = Member::payable()->get()
|
||||
->groupBy(fn ($member) => "{$member->bill_kind->value}{$member->lastname}{$member->address}{$member->zip}{$member->location}");
|
||||
foreach ($memberGroup as $members) {
|
||||
$invoice = Invoice::createForMember($members->first());
|
||||
|
||||
foreach ($members as $member) {
|
||||
foreach ($member->subscription->children as $child) {
|
||||
$invoice->positions()->create([
|
||||
'description' => str($child->name)->replace('{name}', $member->firstname . ' ' . $member->lastname)->replace('{year}', $year),
|
||||
'price' => $child->amount,
|
||||
'member_id' => $member->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$invoices->push($invoice);
|
||||
}
|
||||
|
||||
event(new InvoicesMassStored($year, $invoices));
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->startJob($request->year);
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $parameters
|
||||
*/
|
||||
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
|
||||
{
|
||||
return $jobState
|
||||
->after('Zahlungen erstellt')
|
||||
->failed('Fehler beim Erstellen von Zahlungen')
|
||||
->shouldReload(JobChannels::make()->add('invoice'));
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Actions;
|
||||
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Invoice\Resources\InvoicePositionResource;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PaymentPositionIndexAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return Collection<int, InvoicePosition>
|
||||
*/
|
||||
public function handle(Member $member): Collection
|
||||
{
|
||||
return $member->load('invoicePositions.invoice')->invoicePositions;
|
||||
}
|
||||
|
||||
public function asController(Member $member): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => InvoicePositionResource::collection($this->handle($member)),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -5,8 +5,12 @@ namespace App\Invoice;
|
|||
use App\Payment\Payment;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class BillDocument extends InvoiceDocument
|
||||
class BillDocument extends Invoice
|
||||
{
|
||||
public function linkLabel(): string
|
||||
{
|
||||
return 'Rechnung erstellen';
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
{
|
||||
|
@ -17,4 +21,42 @@ class BillDocument extends InvoiceDocument
|
|||
{
|
||||
return 'tex.bill';
|
||||
}
|
||||
|
||||
public static function sendAllLabel(): string
|
||||
{
|
||||
return 'Rechnungen versenden';
|
||||
}
|
||||
|
||||
public function afterSingle(Payment $payment): void
|
||||
{
|
||||
$payment->update(['status_id' => 2]);
|
||||
}
|
||||
|
||||
public function getMailSubject(): string
|
||||
{
|
||||
return 'Jahresrechnung';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HasMany<Payment> $query
|
||||
*
|
||||
* @return HasMany<Payment>
|
||||
*/
|
||||
public static function paymentsQuery(HasMany $query): HasMany
|
||||
{
|
||||
return $query->whereNeedsBill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Descriptions for sendpayment page.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function getDescription(): array
|
||||
{
|
||||
return [
|
||||
'Diese Funktion erstellt ein PDF mit allen noch nicht versendenden Rechnungen bei den Mitgliedern die Post als Versandweg haben.',
|
||||
'Die Rechnungen werden automatisch auf "Rechnung gestellt" aktualisiert.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Invoice\Queries\InvoiceMemberQuery;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class DocumentFactory
|
||||
{
|
||||
/**
|
||||
* @var array<int, class-string<Invoice>>
|
||||
*/
|
||||
private array $types = [
|
||||
BillDocument::class,
|
||||
RememberDocument::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* @return Collection<int, class-string<Invoice>>
|
||||
*/
|
||||
public function getTypes(): Collection
|
||||
{
|
||||
return collect($this->types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Invoice> $type
|
||||
*/
|
||||
public function singleInvoice(string $type, InvoiceMemberQuery $query): ?Invoice
|
||||
{
|
||||
$pages = $query->getPages($type);
|
||||
|
||||
if ($pages->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->resolve($type, $pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Invoice> $type
|
||||
*
|
||||
* @return Collection<int, Invoice>
|
||||
*/
|
||||
public function invoiceCollection(string $type, InvoiceMemberQuery $query): Collection
|
||||
{
|
||||
return $query
|
||||
->getPages($type)
|
||||
->map(fn ($page) => $this->resolve($type, collect([$page])));
|
||||
}
|
||||
|
||||
public function afterSingle(Invoice $invoice): void
|
||||
{
|
||||
foreach ($invoice->allPayments() as $payment) {
|
||||
$invoice->afterSingle($payment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Invoice> $type
|
||||
* @param Collection<int, Page> $pages
|
||||
*/
|
||||
private function resolve(string $type, Collection $pages): Invoice
|
||||
{
|
||||
return new $type($pages);
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Enums;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
enum InvoiceStatus: string
|
||||
{
|
||||
case NEW = 'Neu';
|
||||
case SENT = 'Rechnung gestellt';
|
||||
case PAID = 'Rechnung beglichen';
|
||||
|
||||
/**
|
||||
* @return Collection<int, string>
|
||||
*/
|
||||
public static function values(): Collection
|
||||
{
|
||||
return collect(static::cases())->map(fn ($case) => $case->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: string, name: string}>
|
||||
*/
|
||||
public static function forSelect(): array
|
||||
{
|
||||
return array_map(fn ($case) => ['id' => $case->value, 'name' => $case->value], static::cases());
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Events;
|
||||
|
||||
use App\Invoice\Models\Invoice;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InvoicesMassStored
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Collection<int, Invoice> $invoices
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public int $year, public Collection $invoices)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new PrivateChannel('channel-name');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Payment\Payment;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Generator;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Zoomyboy\Tex\Document;
|
||||
use Zoomyboy\Tex\Engine;
|
||||
use Zoomyboy\Tex\Template;
|
||||
|
||||
abstract class Invoice extends Document
|
||||
{
|
||||
abstract public function getSubject(): string;
|
||||
|
||||
abstract public function view(): string;
|
||||
|
||||
abstract public function linkLabel(): string;
|
||||
|
||||
abstract public static function sendAllLabel(): string;
|
||||
|
||||
/**
|
||||
* @param HasMany<Payment> $query
|
||||
*
|
||||
* @return HasMany<Payment>
|
||||
*/
|
||||
abstract public static function paymentsQuery(HasMany $query): HasMany;
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
abstract public static function getDescription(): array;
|
||||
|
||||
abstract public function afterSingle(Payment $payment): void;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Page>
|
||||
*/
|
||||
public Collection $pages;
|
||||
public string $subject;
|
||||
protected string $filename;
|
||||
public string $until;
|
||||
public InvoiceSettings $settings;
|
||||
|
||||
/**
|
||||
* @param Collection<int, Page> $pages
|
||||
*/
|
||||
public function __construct(Collection $pages)
|
||||
{
|
||||
$this->pages = $pages;
|
||||
$this->subject = $this->getSubject();
|
||||
$this->until = now()->addWeeks(2)->format('d.m.Y');
|
||||
$this->setFilename(Str::slug("{$this->getSubject()} für {$pages->first()?->familyName}"));
|
||||
$this->settings = app(InvoiceSettings::class);
|
||||
}
|
||||
|
||||
public function number(int $number): string
|
||||
{
|
||||
return number_format($number / 100, 2, '.', '');
|
||||
}
|
||||
|
||||
public function getUntil(): Carbon
|
||||
{
|
||||
return now()->addWeeks(2);
|
||||
}
|
||||
|
||||
public function getEngine(): Engine
|
||||
{
|
||||
return Engine::PDFLATEX;
|
||||
}
|
||||
|
||||
public function basename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
public function template(): Template
|
||||
{
|
||||
return Template::make('tex.templates.default');
|
||||
}
|
||||
|
||||
public function setFilename(string $filename): self
|
||||
{
|
||||
$this->filename = $filename;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRecipient(): MailRecipient
|
||||
{
|
||||
if (!$this->pages->first()?->email) {
|
||||
throw new Exception('Cannot get Recipient. Mail not set.');
|
||||
}
|
||||
|
||||
return new MailRecipient($this->pages->first()->email, $this->pages->first()->familyName);
|
||||
}
|
||||
|
||||
public function allPayments(): Generator
|
||||
{
|
||||
foreach ($this->pages as $page) {
|
||||
foreach ($page->getPayments() as $payment) {
|
||||
yield $payment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return view-string
|
||||
*/
|
||||
public function mailView(): string
|
||||
{
|
||||
$view = 'mail.payment.'.Str::snake(class_basename($this));
|
||||
|
||||
if (!view()->exists($view)) {
|
||||
throw new Exception('Mail view '.$view.' existiert nicht.');
|
||||
}
|
||||
|
||||
return $view;
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Payment\Payment;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
use Zoomyboy\Tex\Document;
|
||||
use Zoomyboy\Tex\Engine;
|
||||
use Zoomyboy\Tex\Template;
|
||||
|
||||
abstract class InvoiceDocument extends Document
|
||||
{
|
||||
abstract public function getSubject(): string;
|
||||
abstract public function view(): string;
|
||||
|
||||
public string $until;
|
||||
public string $filename;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $positions
|
||||
*/
|
||||
public function __construct(
|
||||
public string $toName,
|
||||
public string $toAddress,
|
||||
public string $toZip,
|
||||
public string $toLocation,
|
||||
public string $greeting,
|
||||
public array $positions,
|
||||
public string $usage,
|
||||
) {
|
||||
$this->until = now()->addWeeks(2)->format('d.m.Y');
|
||||
$this->filename = Str::slug("{$this->getSubject()} für {$toName}");
|
||||
}
|
||||
|
||||
public static function fromInvoice(Invoice $invoice): self
|
||||
{
|
||||
return static::withoutMagicalCreationFrom([
|
||||
'toName' => $invoice->to['name'],
|
||||
'toAddress' => $invoice->to['address'],
|
||||
'toZip' => $invoice->to['zip'],
|
||||
'toLocation' => $invoice->to['location'],
|
||||
'greeting' => $invoice->greeting,
|
||||
'positions' => static::renderPositions($invoice),
|
||||
'usage' => $invoice->usage,
|
||||
]);
|
||||
}
|
||||
|
||||
public function settings(): InvoiceSettings
|
||||
{
|
||||
return app(InvoiceSettings::class);
|
||||
}
|
||||
|
||||
public function getEngine(): Engine
|
||||
{
|
||||
return Engine::PDFLATEX;
|
||||
}
|
||||
|
||||
public function basename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
public function template(): Template
|
||||
{
|
||||
return Template::make('tex.templates.default');
|
||||
}
|
||||
|
||||
public function setFilename(string $filename): self
|
||||
{
|
||||
$this->filename = $filename;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function renderPositions(Invoice $invoice): array
|
||||
{
|
||||
return $invoice->positions->mapWithKeys(fn ($position) => [$position->description => static::number($position->price)])->toArray();
|
||||
}
|
||||
|
||||
public static function number(int $number): string
|
||||
{
|
||||
return number_format($number / 100, 2, '.', '');
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Mails;
|
||||
|
||||
use App\Invoice\Models\Invoice;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class BillMail extends Mailable
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public Invoice $invoice, public string $filename)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->markdown('mail.invoice.bill')
|
||||
->attach($this->filename)
|
||||
->replyTo('kasse@stamm-silva.de')
|
||||
->subject('Rechnung | DPSG Stamm Silva');
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Mails;
|
||||
|
||||
use App\Invoice\Models\Invoice;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RememberMail extends Mailable
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public Invoice $invoice, public string $filename)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->markdown('mail.invoice.remember')
|
||||
->attach($this->filename)
|
||||
->replyTo('kasse@stamm-silva.de')
|
||||
->subject('Zahlungserinnerung | DPSG Stamm Silva');
|
||||
}
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Models;
|
||||
|
||||
use App\Invoice\BillDocument;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\InvoiceDocument;
|
||||
use App\Invoice\RememberDocument;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use stdClass;
|
||||
|
||||
class Invoice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public $casts = [
|
||||
'to' => 'json',
|
||||
'status' => InvoiceStatus::class,
|
||||
'via' => BillKind::class,
|
||||
];
|
||||
|
||||
/** @var array<int, string> */
|
||||
public $dates = [
|
||||
'sent_at',
|
||||
'last_remembered_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return HasMany<InvoicePosition>
|
||||
*/
|
||||
public function positions(): HasMany
|
||||
{
|
||||
return $this->hasMany(InvoicePosition::class);
|
||||
}
|
||||
|
||||
public static function createForMember(Member $member): self
|
||||
{
|
||||
return static::create([
|
||||
'to' => [
|
||||
'name' => 'Familie ' . $member->lastname,
|
||||
'address' => $member->address,
|
||||
'zip' => $member->zip,
|
||||
'location' => $member->location,
|
||||
],
|
||||
'greeting' => 'Liebe Familie ' . $member->lastname,
|
||||
'status' => InvoiceStatus::NEW,
|
||||
'via' => $member->bill_kind,
|
||||
'usage' => 'Mitgliedsbeitrag für ' . $member->lastname,
|
||||
'mail_email' => $member->email_parents ?: $member->email,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function booted(): void
|
||||
{
|
||||
static::deleting(function ($model) {
|
||||
$model->positions()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWhereNeedsPayment(Builder $query): Builder
|
||||
{
|
||||
return $query->whereIn('status', [InvoiceStatus::NEW->value, InvoiceStatus::SENT->value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWhereNeedsBill(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', InvoiceStatus::NEW);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWhereNeedsRemember(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->where('status', InvoiceStatus::SENT)
|
||||
->whereNotNull('sent_at')
|
||||
->whereNotNull('last_remembered_at')
|
||||
->where('last_remembered_at', '<=', now()->subMonths(3));
|
||||
}
|
||||
|
||||
public function getMailRecipient(): stdClass
|
||||
{
|
||||
return (object) [
|
||||
'email' => $this->mail_email,
|
||||
'name' => $this->to['name']
|
||||
];
|
||||
}
|
||||
|
||||
public function sent(InvoiceDocument $document): void
|
||||
{
|
||||
if (is_a($document, BillDocument::class)) {
|
||||
$this->update([
|
||||
'sent_at' => now(),
|
||||
'status' => InvoiceStatus::SENT,
|
||||
'last_remembered_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (is_a($document, RememberDocument::class)) {
|
||||
$this->update([
|
||||
'last_remembered_at' => now(),
|
||||
'status' => InvoiceStatus::SENT,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Models;
|
||||
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class InvoicePosition extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, self>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Invoice, self>
|
||||
*/
|
||||
public function invoice(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public static function booted(): void
|
||||
{
|
||||
static::deleted(function ($model) {
|
||||
if ($model->invoice->positions()->get()->count() === 0) {
|
||||
$model->invoice->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Member\Member;
|
||||
use App\Payment\Payment;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Page
|
||||
{
|
||||
/**
|
||||
* @var Collection<int, Member>
|
||||
*/
|
||||
private Collection $members;
|
||||
public string $familyName;
|
||||
public string $singleName;
|
||||
public string $address;
|
||||
public string $zip;
|
||||
public string $location;
|
||||
public string $usage;
|
||||
public ?string $email;
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public array $positions;
|
||||
|
||||
/**
|
||||
* @param Collection<int, Member> $members
|
||||
*/
|
||||
public function __construct(Collection $members)
|
||||
{
|
||||
$this->members = $members;
|
||||
$this->familyName = $this->members->first()->lastname;
|
||||
$this->singleName = $members->first()->lastname;
|
||||
$this->address = $members->first()->address;
|
||||
$this->zip = $members->first()->zip;
|
||||
$this->location = $members->first()->location;
|
||||
$this->email = $members->first()->email_parents ?: $members->first()->email;
|
||||
$this->positions = $this->getPositions();
|
||||
$this->usage = "Mitgliedsbeitrag für {$this->familyName}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getPositions(): array
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
$result = [];
|
||||
|
||||
foreach ($this->getPayments() as $payment) {
|
||||
if ($payment->subscription->split) {
|
||||
foreach ($payment->subscription->children as $child) {
|
||||
$result["{$payment->subscription->name} ({$child->name}) {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname}"] = $this->number($child->amount);
|
||||
}
|
||||
} else {
|
||||
$result["{$payment->subscription->name} {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname}"] = $this->number($payment->subscription->getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Payment>
|
||||
*/
|
||||
public function getPayments(): Collection
|
||||
{
|
||||
return $this->members->pluck('payments')->flatten(1);
|
||||
}
|
||||
|
||||
public function number(int $number): string
|
||||
{
|
||||
return number_format($number / 100, 2, '.', '');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Queries;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class BillKindQuery extends InvoiceMemberQuery
|
||||
{
|
||||
public function __construct(
|
||||
private BillKind $billKind
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<Member>
|
||||
*/
|
||||
protected function getQuery(): Builder
|
||||
{
|
||||
return Member::where('bill_kind', $this->billKind);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Queries;
|
||||
|
||||
use App\Invoice\Invoice;
|
||||
use App\Invoice\Page;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class InvoiceMemberQuery
|
||||
{
|
||||
/**
|
||||
* @return Builder<Member>
|
||||
*/
|
||||
abstract protected function getQuery(): Builder;
|
||||
|
||||
/**
|
||||
* @param class-string<Invoice> $type
|
||||
*
|
||||
* @return Collection<int, Page>
|
||||
*/
|
||||
public function getPages(string $type): Collection
|
||||
{
|
||||
return $this->get($type)->groupBy(
|
||||
fn ($member) => Str::slug(
|
||||
"{$member->lastname}{$member->address}{$member->zip}{$member->location}",
|
||||
),
|
||||
)->map(fn ($page) => new Page($page));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Invoice> $type
|
||||
*
|
||||
* @return EloquentCollection<int, Member>
|
||||
*/
|
||||
private function get(string $type): EloquentCollection
|
||||
{
|
||||
return $this->getQuery()
|
||||
->with([
|
||||
'payments' => fn ($query) => $type::paymentsQuery($query)
|
||||
->orderByRaw('nr, member_id'),
|
||||
])
|
||||
->get()
|
||||
->filter(fn (Member $member) => $member->payments->count() > 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Queries;
|
||||
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SingleMemberQuery extends InvoiceMemberQuery
|
||||
{
|
||||
public function __construct(
|
||||
private Member $member
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<Member>
|
||||
*/
|
||||
protected function getQuery(): Builder
|
||||
{
|
||||
return Member::where($this->member->only(['lastname', 'address', 'zip', 'location']));
|
||||
}
|
||||
}
|
|
@ -5,8 +5,12 @@ namespace App\Invoice;
|
|||
use App\Payment\Payment;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class RememberDocument extends InvoiceDocument
|
||||
class RememberDocument extends Invoice
|
||||
{
|
||||
public function linkLabel(): string
|
||||
{
|
||||
return 'Erinnerung erstellen';
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
{
|
||||
|
@ -17,4 +21,42 @@ class RememberDocument extends InvoiceDocument
|
|||
{
|
||||
return 'tex.remember';
|
||||
}
|
||||
|
||||
public static function sendAllLabel(): string
|
||||
{
|
||||
return 'Erinnerungen versenden';
|
||||
}
|
||||
|
||||
public function afterSingle(Payment $payment): void
|
||||
{
|
||||
$payment->update(['last_remembered_at' => now()]);
|
||||
}
|
||||
|
||||
public function getMailSubject(): string
|
||||
{
|
||||
return 'Zahlungserinnerung';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HasMany<Payment> $query
|
||||
*
|
||||
* @return HasMany<Payment>
|
||||
*/
|
||||
public static function paymentsQuery(HasMany $query): HasMany
|
||||
{
|
||||
return $query->whereNeedsRemember();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Descriptions for sendpayment page.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function getDescription(): array
|
||||
{
|
||||
return [
|
||||
'Diese Funktion erstellt Erinnerungs-PDFs mit allen versendeten aber noch nich bezahlten Rechnungen bei den Mitgliedern die Post als Versandweg haben.',
|
||||
'Das zuletzt erinnerte Datum wird auf heute gesetzt.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Resources;
|
||||
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin InvoicePosition
|
||||
*/
|
||||
class InvoicePositionResource extends JsonResource
|
||||
{
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'price' => $this->price,
|
||||
'member_id' => $this->member_id,
|
||||
'description' => $this->description,
|
||||
'invoice' => new InvoiceResource($this->whenLoaded('invoice')),
|
||||
'price_human' => number_format($this->price / 100, 2, ',', '') . ' €',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice\Resources;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Lib\HasMeta;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Invoice
|
||||
*/
|
||||
class InvoiceResource extends JsonResource
|
||||
{
|
||||
|
||||
use HasMeta;
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'to' => $this->to,
|
||||
'sum_human' => number_format($this->positions->sum('price') / 100, 2, ',', '') . ' €',
|
||||
'sent_at_human' => $this->sent_at?->format('d.m.Y') ?: '',
|
||||
'status' => $this->status->value,
|
||||
'via' => $this->via->value,
|
||||
'positions' => InvoicePositionResource::collection($this->whenLoaded('positions')),
|
||||
'greeting' => $this->greeting,
|
||||
'usage' => $this->usage,
|
||||
'mail_email' => $this->mail_email,
|
||||
'links' => [
|
||||
'pdf' => route('invoice.pdf', ['invoice' => $this->getModel()]),
|
||||
'rememberpdf' => route('invoice.rememberpdf', ['invoice' => $this->getModel()]),
|
||||
'update' => route('invoice.update', ['invoice' => $this->getModel()]),
|
||||
'destroy' => route('invoice.destroy', ['invoice' => $this->getModel()]),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
'links' => [
|
||||
'mass-store' => route('invoice.mass-store'),
|
||||
'store' => route('invoice.store'),
|
||||
'masspdf' => route('invoice.masspdf'),
|
||||
],
|
||||
'vias' => BillKind::forSelect(),
|
||||
'statuses' => InvoiceStatus::forSelect(),
|
||||
'members' => Member::forSelect(),
|
||||
'default' => [
|
||||
'to' => [
|
||||
'name' => '',
|
||||
'address' => '',
|
||||
'zip' => '',
|
||||
'location' => '',
|
||||
],
|
||||
'positions' => [],
|
||||
'greeting' => '',
|
||||
'status' => InvoiceStatus::NEW->value,
|
||||
'via' => null,
|
||||
'usage' => '',
|
||||
'mail_email' => '',
|
||||
],
|
||||
'default_position' => [
|
||||
'id' => null,
|
||||
'price' => 0,
|
||||
'description' => '',
|
||||
'member_id' => null,
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Succeeded implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
final private function __construct(public string $message)
|
||||
{
|
||||
}
|
||||
|
||||
public static function message(string $message): self
|
||||
{
|
||||
return new self($message);
|
||||
}
|
||||
|
||||
public function dispatch(): void
|
||||
{
|
||||
event($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, Channel>
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return [
|
||||
new Channel('jobs'),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ trait TracksJob
|
|||
{
|
||||
$jobId = Str::uuid();
|
||||
$jobState = WithJobState::make($jobId);
|
||||
tap($this->jobState(...[$jobState, ...$parameters])->beforeMessage, fn ($beforeMessage) => $beforeMessage && $beforeMessage->dispatch());;
|
||||
$this->jobState(...[$jobState, ...$parameters])->beforeMessage->dispatch();
|
||||
$parameters[] = $jobId;
|
||||
static::dispatch(...$parameters);
|
||||
}
|
||||
|
|
|
@ -18,10 +18,9 @@ class MemberShowAction
|
|||
public function handle(Member $member): array
|
||||
{
|
||||
return [
|
||||
'data' => new MemberResource(
|
||||
$member
|
||||
'data' => new MemberResource($member
|
||||
->load('memberships')
|
||||
->load('invoicePositions.invoice')
|
||||
->load('payments.subscription.children')
|
||||
->load('nationality')
|
||||
->load('region')
|
||||
->load('subscription')
|
||||
|
|
|
@ -8,9 +8,9 @@ use App\Course\Models\CourseMember;
|
|||
use App\Gender;
|
||||
use App\Group;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Nami\HasNamiField;
|
||||
use App\Nationality;
|
||||
use App\Payment\Payment;
|
||||
use App\Payment\Subscription;
|
||||
use App\Pdf\Sender;
|
||||
use App\Region;
|
||||
|
@ -110,6 +110,13 @@ class Member extends Model implements Geolocatable
|
|||
$this->update(['version' => $version]);
|
||||
}
|
||||
|
||||
public function createPayment(array $attributes): void
|
||||
{
|
||||
$this->payments()->create(array_merge($attributes, [
|
||||
'last_remembered_at' => now(),
|
||||
]));
|
||||
}
|
||||
|
||||
// ----------------------------------- Getters -----------------------------------
|
||||
public function getFullnameAttribute(): string
|
||||
{
|
||||
|
@ -206,14 +213,6 @@ class Member extends Model implements Geolocatable
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<InvoicePosition>
|
||||
*/
|
||||
public function invoicePositions(): HasMany
|
||||
{
|
||||
return $this->hasMany(InvoicePosition::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Confession, self>
|
||||
*/
|
||||
|
@ -262,6 +261,14 @@ class Member extends Model implements Geolocatable
|
|||
return $this->hasMany(Membership::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Payment>
|
||||
*/
|
||||
public function payments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Payment::class)->orderBy('nr');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Membership>
|
||||
*/
|
||||
|
@ -281,11 +288,9 @@ class Member extends Model implements Geolocatable
|
|||
public static function booted()
|
||||
{
|
||||
static::deleting(function (self $model): void {
|
||||
$model->payments->each->delete();
|
||||
$model->memberships->each->delete();
|
||||
$model->courses->each->delete();
|
||||
$model->invoicePositions->each(function ($position) {
|
||||
$position->delete();
|
||||
});
|
||||
});
|
||||
|
||||
static::saving(fn ($model) => $model->updateSearch());
|
||||
|
@ -310,9 +315,11 @@ class Member extends Model implements Geolocatable
|
|||
public function scopeWithPendingPayment(Builder $query): Builder
|
||||
{
|
||||
return $query->addSelect([
|
||||
'pending_payment' => InvoicePosition::selectRaw('SUM(price)')
|
||||
->whereColumn('invoice_positions.member_id', 'members.id')
|
||||
->whereHas('invoice', fn ($query) => $query->whereNeedsPayment())
|
||||
'pending_payment' => Payment::selectRaw('SUM(subscription_children.amount)')
|
||||
->whereColumn('payments.member_id', 'members.id')
|
||||
->whereNeedsPayment()
|
||||
->join('subscriptions', 'subscriptions.id', 'payments.subscription_id')
|
||||
->join('subscription_children', 'subscriptions.id', 'subscription_children.parent_id'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -323,7 +330,9 @@ class Member extends Model implements Geolocatable
|
|||
*/
|
||||
public function scopeWhereHasPendingPayment(Builder $query): Builder
|
||||
{
|
||||
return $query->whereHas('invoicePositions', fn ($q) => $q->whereHas('invoice', fn ($q) => $q->whereNeedsPayment()));
|
||||
return $query->whereHas('payments', function (Builder $q): void {
|
||||
$q->whereNeedsPayment();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -333,8 +342,8 @@ class Member extends Model implements Geolocatable
|
|||
*/
|
||||
public function scopeWhereAusstand(Builder $query): Builder
|
||||
{
|
||||
return $query->whereHas('invoicePositions', function ($q) {
|
||||
return $q->whereHas('invoice', fn ($query) => $query->whereNeedsPayment());
|
||||
return $query->whereHas('payments', function ($q) {
|
||||
return $q->whereHas('status', fn ($q) => $q->where('is_remember', true));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -348,6 +357,18 @@ class Member extends Model implements Geolocatable
|
|||
return $query->where('bill_kind', '!=', null)->where('subscription_id', '!=', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWhereNoPayment(Builder $query, int $year): Builder
|
||||
{
|
||||
return $query->whereDoesntHave('payments', function (Builder $q) use ($year) {
|
||||
$q->where('nr', '=', $year);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
|
@ -485,14 +506,6 @@ class Member extends Model implements Geolocatable
|
|||
])->implode(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: int, name: string}>
|
||||
*/
|
||||
public static function forSelect(): array
|
||||
{
|
||||
return static::select(['id', 'firstname', 'lastname'])->get()->map(fn ($member) => ['id' => $member->id, 'name' => $member->fullname])->toArray();
|
||||
}
|
||||
|
||||
// -------------------------------- Geolocation --------------------------------
|
||||
// *****************************************************************************
|
||||
public function fillCoordinate(Coordinate $coordinate): void
|
||||
|
|
|
@ -8,13 +8,14 @@ use App\Course\Models\Course;
|
|||
use App\Course\Resources\CourseMemberResource;
|
||||
use App\Gender;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Resources\InvoicePositionResource;
|
||||
use App\Lib\HasMeta;
|
||||
use App\Member\Data\NestedGroup;
|
||||
use App\Member\Resources\NationalityResource;
|
||||
use App\Member\Resources\RegionResource;
|
||||
use App\Membership\MembershipResource;
|
||||
use App\Nationality;
|
||||
use App\Payment\PaymentResource;
|
||||
use App\Payment\Status;
|
||||
use App\Payment\Subscription;
|
||||
use App\Payment\SubscriptionResource;
|
||||
use App\Region;
|
||||
|
@ -71,11 +72,11 @@ class MemberResource extends JsonResource
|
|||
'bill_kind_name' => optional($this->bill_kind)->value,
|
||||
'has_nami' => null !== $this->nami_id,
|
||||
'children_phone' => $this->children_phone,
|
||||
'payments' => PaymentResource::collection($this->whenLoaded('payments')),
|
||||
'pending_payment' => $this->pending_payment ? number_format($this->pending_payment / 100, 2, ',', '.') . ' €' : null,
|
||||
'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug,
|
||||
'courses' => CourseMemberResource::collection($this->whenLoaded('courses')),
|
||||
'memberships' => MembershipResource::collection($this->whenLoaded('memberships')),
|
||||
'invoicePositions' => InvoicePositionResource::collection($this->whenLoaded('invoicePositions')),
|
||||
'nationality' => new NationalityResource($this->whenLoaded('nationality')),
|
||||
'region' => new RegionResource($this->whenLoaded('region')),
|
||||
'full_address' => $this->fullAddress,
|
||||
|
@ -108,7 +109,7 @@ class MemberResource extends JsonResource
|
|||
'group_name' => $this->group->name,
|
||||
'links' => [
|
||||
'membership_index' => route('member.membership.index', ['member' => $this->getModel()]),
|
||||
'invoiceposition_index' => route('member.invoice-position.index', ['member' => $this->getModel()]),
|
||||
'payment_index' => route('member.payment.index', ['member' => $this->getModel()]),
|
||||
'course_index' => route('member.course.index', ['member' => $this->getModel()]),
|
||||
'show' => route('member.show', ['member' => $this->getModel()]),
|
||||
'edit' => route('member.edit', ['member' => $this->getModel()]),
|
||||
|
@ -156,6 +157,8 @@ class MemberResource extends JsonResource
|
|||
'links' => [
|
||||
'index' => route('member.index'),
|
||||
'create' => route('member.create'),
|
||||
'allpayment' => route('allpayment.page'),
|
||||
'sendpayment' => route('sendpayment.create'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class MembershipResource extends JsonResource
|
|||
],
|
||||
'groups' => NestedGroup::cacheForSelect(),
|
||||
'activities' => $activities->map(fn ($activity) => ['id' => $activity->id, 'name' => $activity->name]),
|
||||
'subactivities' => $activities->mapWithKeys(fn ($activity) => [$activity->id => $activity->subactivities->map(fn ($subactivity) => ['id' => $subactivity->id, 'name' => $subactivity->name, 'is_age_group' => $subactivity->is_age_group])]),
|
||||
'subactivities' => $activities->mapWithKeys(fn ($activity) => [$activity->id => $activity->subactivities->map(fn ($subactivity) => ['id' => $subactivity->id, 'name' => $subactivity->name])]),
|
||||
'default' => [
|
||||
'group_id' => $member->group_id,
|
||||
'activity_id' => null,
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment;
|
||||
|
||||
use App\Invoice\DocumentFactory;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ActionFactory
|
||||
{
|
||||
/**
|
||||
* @return Collection<int, array{link: array{href: string, label: mixed}, text: mixed}>
|
||||
*/
|
||||
public function allLinks(): Collection
|
||||
{
|
||||
return app(DocumentFactory::class)->getTypes()->map(function ($repo) {
|
||||
return [
|
||||
'link' => [
|
||||
'href' => route('sendpayment.pdf', ['type' => $repo]),
|
||||
'label' => $repo::sendAllLabel(),
|
||||
],
|
||||
'text' => $repo::getDescription(),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment\Actions;
|
||||
|
||||
use Inertia;
|
||||
use Inertia\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class AllpaymentPageAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function handle(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function asController(): Response
|
||||
{
|
||||
session()->put('menu', 'member');
|
||||
session()->put('title', 'Rechnungen erstellen');
|
||||
|
||||
return Inertia::render('allpayment/VForm', $this->handle());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment\Actions;
|
||||
|
||||
use App\Member\Member;
|
||||
use App\Member\Membership;
|
||||
use App\Payment\Status;
|
||||
use App\Payment\Subscription;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class AllpaymentStoreAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => 'required|numeric',
|
||||
'for_promise' => 'present|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(int $year, bool $forPromise): void
|
||||
{
|
||||
foreach (Member::payable()->whereNoPayment($year)->get() as $member) {
|
||||
$member->createPayment([
|
||||
'nr' => $year,
|
||||
'subscription_id' => $member->subscription_id,
|
||||
'status_id' => Status::default(),
|
||||
]);
|
||||
|
||||
if (!$forPromise) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createPaymentsForPromise($member, $year);
|
||||
}
|
||||
}
|
||||
|
||||
private function createPaymentsForPromise(Member $member, int $year): void
|
||||
{
|
||||
$subscription = Subscription::firstWhere('for_promise', true);
|
||||
|
||||
if (is_null($subscription)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->promisedMemberships($member, $year) as $membership) {
|
||||
$attributes = [
|
||||
'nr' => $membership->subactivity->name.' '.$membership->promised_at->year,
|
||||
'subscription_id' => $subscription->id,
|
||||
];
|
||||
|
||||
if (!$member->payments()->where($attributes)->exists()) {
|
||||
$member->createPayment([
|
||||
...$attributes,
|
||||
'status_id' => Status::default(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Membership>
|
||||
*/
|
||||
public function promisedMemberships(Member $member, int $year): Collection
|
||||
{
|
||||
return $member->memberships()->whereNotNull('promised_at')->whereYear('promised_at', now()->year($year)->subYear())->get();
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): RedirectResponse
|
||||
{
|
||||
$this->handle($request->year, $request->for_promise);
|
||||
|
||||
return redirect()->back()->success('Zahlungen erstellt');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment\Actions;
|
||||
|
||||
use App\Member\Member;
|
||||
use App\Payment\Payment;
|
||||
use App\Payment\PaymentResource;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class IndexAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return Collection<int, Payment>
|
||||
*/
|
||||
public function handle(Member $member): Collection
|
||||
{
|
||||
return $member->payments()->with('subscription')->get();
|
||||
}
|
||||
|
||||
public function asController(Member $member): AnonymousResourceCollection
|
||||
{
|
||||
return PaymentResource::collection($this->handle($member))
|
||||
->additional([
|
||||
'meta' => PaymentResource::memberMeta($member),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment\Actions;
|
||||
|
||||
use App\Lib\JobMiddleware\JobChannels;
|
||||
use App\Lib\JobMiddleware\WithJobState;
|
||||
use App\Lib\Queue\TracksJob;
|
||||
use App\Payment\Payment;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PaymentDestroyAction
|
||||
{
|
||||
use AsAction;
|
||||
use TracksJob;
|
||||
|
||||
public function handle(int $paymentId): void
|
||||
{
|
||||
Payment::find($paymentId)->delete();
|
||||
}
|
||||
|
||||
public function asController(Payment $payment): JsonResponse
|
||||
{
|
||||
$this->startJob($payment->id, $payment->member->fullname);
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $parameters
|
||||
*/
|
||||
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
|
||||
{
|
||||
$memberName = $parameters[1];
|
||||
|
||||
return $jobState
|
||||
->before('Zahlung für ' . $memberName . ' wird gelöscht')
|
||||
->after('Zahlung für ' . $memberName . ' gelöscht')
|
||||
->failed('Fehler beim Löschen der Zahlung für ' . $memberName)
|
||||
->shouldReload(JobChannels::make()->add('member')->add('payment'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment\Actions;
|
||||
|
||||
use App\Lib\JobMiddleware\JobChannels;
|
||||
use App\Lib\JobMiddleware\WithJobState;
|
||||
use App\Lib\Queue\TracksJob;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PaymentStoreAction
|
||||
{
|
||||
use AsAction;
|
||||
use TracksJob;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function handle(Member $member, array $attributes): void
|
||||
{
|
||||
$member->createPayment($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'nr' => 'required',
|
||||
'subscription_id' => 'required|exists:subscriptions,id',
|
||||
'status_id' => 'required|exists:statuses,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(Member $member, ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->startJob($member, $request->validated());
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $parameters
|
||||
*/
|
||||
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
|
||||
{
|
||||
$member = $parameters[0];
|
||||
|
||||
return $jobState
|
||||
->before('Zahlung für ' . $member->fullname . ' wird gespeichert')
|
||||
->after('Zahlung für ' . $member->fullname . ' gespeichert')
|
||||
->failed('Fehler beim Erstellen der Zahlung für ' . $member->fullname)
|
||||
->shouldReload(JobChannels::make()->add('member')->add('payment'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment\Actions;
|
||||
|
||||
use App\Lib\JobMiddleware\JobChannels;
|
||||
use App\Lib\JobMiddleware\WithJobState;
|
||||
use App\Lib\Queue\TracksJob;
|
||||
use App\Payment\Payment;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PaymentUpdateAction
|
||||
{
|
||||
use AsAction;
|
||||
use TracksJob;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function handle(Payment $payment, array $attributes): void
|
||||
{
|
||||
$payment->update($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'nr' => 'required',
|
||||
'subscription_id' => 'required|exists:subscriptions,id',
|
||||
'status_id' => 'required|exists:statuses,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(Payment $payment, ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->startJob($payment, $request->validated());
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $parameters
|
||||
*/
|
||||
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
|
||||
{
|
||||
$member = $parameters[0]->member;
|
||||
|
||||
return $jobState
|
||||
->before('Zahlung für ' . $member->fullname . ' wird aktualisiert')
|
||||
->after('Zahlung für ' . $member->fullname . ' aktualisiert')
|
||||
->failed('Fehler beim Aktualisieren der Zahlung für ' . $member->fullname)
|
||||
->shouldReload(JobChannels::make()->add('member')->add('payment'));
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice;
|
||||
namespace App\Payment;
|
||||
|
||||
use App\Dashboard\Blocks\Block;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Member\Member;
|
||||
|
||||
class MemberPaymentBlock extends Block
|
||||
|
@ -13,15 +12,17 @@ class MemberPaymentBlock extends Block
|
|||
*/
|
||||
public function data(): array
|
||||
{
|
||||
$amount = InvoicePosition::whereHas('invoice', fn ($query) => $query->whereNeedsPayment())
|
||||
->selectRaw('sum(price) AS price')
|
||||
$amount = Payment::whereNeedsPayment()
|
||||
->selectRaw('sum(subscription_children.amount) AS nr')
|
||||
->join('subscriptions', 'subscriptions.id', 'payments.subscription_id')
|
||||
->join('subscription_children', 'subscriptions.id', 'subscription_children.parent_id')
|
||||
->first();
|
||||
$members = Member::whereHasPendingPayment()->count();
|
||||
|
||||
return [
|
||||
'members' => $members,
|
||||
'total_members' => Member::count(),
|
||||
'amount' => number_format((int) $amount->price / 100, 2, ',', '.') . ' €',
|
||||
'amount' => number_format((int) $amount->nr / 100, 2, ',', '.').' €',
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment;
|
||||
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Payment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = ['member_id', 'subscription_id', 'nr', 'status_id', 'last_remembered_at'];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, self>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Subscription, self>
|
||||
*/
|
||||
public function subscription(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Subscription::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Status, self>
|
||||
*/
|
||||
public function status(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Status::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWhereNeedsPayment(Builder $query): Builder
|
||||
{
|
||||
return $query->whereHas('status', function ($q) {
|
||||
return $q->needsPayment();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWhereNeedsBill(Builder $query): Builder
|
||||
{
|
||||
return $query->whereHas('status', function ($q) {
|
||||
return $q->where('is_bill', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWhereNeedsRemember(Builder $query): Builder
|
||||
{
|
||||
return $query->whereHas('status', function ($q) {
|
||||
return $q->where('is_remember', true);
|
||||
})->where(fn ($query) => $query->whereNull('last_remembered_at')->orWhere('last_remembered_at', '<=', now()->subMonths(3)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment;
|
||||
|
||||
use App\Invoice\Invoice;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PaymentMail extends Mailable
|
||||
{
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public Invoice $invoice;
|
||||
public string $filename;
|
||||
public string $salutation;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Invoice $invoice, string $filename)
|
||||
{
|
||||
$this->invoice = $invoice;
|
||||
$this->filename = $filename;
|
||||
$this->salutation = 'Liebe Familie '.$invoice->pages->first()->familyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->markdown($this->invoice->mailView())
|
||||
->attach($this->filename)
|
||||
->replyTo('kasse@stamm-silva.de')
|
||||
->subject($this->invoice->getSubject().' | DPSG Stamm Silva');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment;
|
||||
|
||||
use App\Member\Member;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Payment
|
||||
*/
|
||||
class PaymentResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'subscription_id' => $this->subscription_id,
|
||||
'subscription' => new SubscriptionResource($this->whenLoaded('subscription')),
|
||||
'status_name' => $this->status->name,
|
||||
'status_id' => $this->status->id,
|
||||
'nr' => $this->nr,
|
||||
'id' => $this->id,
|
||||
'is_accepted' => $this->status->isAccepted(),
|
||||
'links' => [
|
||||
'update' => route('payment.update', ['payment' => $this->getModel()]),
|
||||
'destroy' => route('payment.destroy', ['payment' => $this->getModel()]),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function memberMeta(Member $member): array
|
||||
{
|
||||
return [
|
||||
'statuses' => Status::forSelect(),
|
||||
'subscriptions' => Subscription::forSelect(),
|
||||
'default' => [
|
||||
'nr' => '',
|
||||
'subscription_id' => null,
|
||||
'status_id' => null
|
||||
],
|
||||
'links' => [
|
||||
'store' => route('member.payment.store', ['member' => $member]),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\DocumentFactory;
|
||||
use App\Invoice\Queries\BillKindQuery;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response as InertiaResponse;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class SendpaymentController extends Controller
|
||||
{
|
||||
public function create(): InertiaResponse
|
||||
{
|
||||
session()->put('menu', 'member');
|
||||
session()->put('title', 'Rechnungen versenden');
|
||||
|
||||
return Inertia::render('sendpayment/VForm', [
|
||||
'types' => app(ActionFactory::class)->allLinks(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response|Responsable
|
||||
*/
|
||||
public function send(Request $request)
|
||||
{
|
||||
$invoice = app(DocumentFactory::class)->singleInvoice($request->type, new BillKindQuery(BillKind::POST));
|
||||
|
||||
if (is_null($invoice)) {
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
$pdfFile = Tex::compile($invoice);
|
||||
app(DocumentFactory::class)->afterSingle($invoice);
|
||||
|
||||
return $pdfFile;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Payment;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Status extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = ['name', 'is_bill', 'is_remember'];
|
||||
public $timestamps = false;
|
||||
public $casts = [
|
||||
'is_bill' => 'boolean',
|
||||
'is_remember' => 'boolean',
|
||||
];
|
||||
|
||||
public static function default(): int
|
||||
{
|
||||
return static::where('is_bill', true)->where('is_remember', true)->first()->id;
|
||||
}
|
||||
|
||||
public function isAccepted(): bool
|
||||
{
|
||||
return false === $this->is_bill && false === $this->is_remember;
|
||||
}
|
||||
|
||||
// ---------------------------------- Scopes -----------------------------------
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeNeedsPayment(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query): Builder {
|
||||
return $query->where('is_bill', true)->orWhere('is_remember', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{name: string, id: int}>
|
||||
*/
|
||||
public static function forSelect(): array
|
||||
{
|
||||
return static::select('name', 'id')->get()->toArray();
|
||||
}
|
||||
}
|
|
@ -15,7 +15,15 @@ class Subscription extends Model
|
|||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public $fillable = ['name', 'fee_id'];
|
||||
public $fillable = ['name', 'fee_id', 'split', 'for_promise'];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $casts = [
|
||||
'split' => 'boolean',
|
||||
'for_promise' => 'boolean',
|
||||
];
|
||||
|
||||
public function getAmount(): int
|
||||
{
|
||||
|
|
|
@ -39,9 +39,12 @@ class SubscriptionController extends Controller
|
|||
{
|
||||
$subscriptionParams = $request->validate([
|
||||
'name' => 'required|max:255',
|
||||
'split' => 'present|boolean',
|
||||
'fee_id' => 'required|exists:fees,id',
|
||||
'for_promise' => 'present|boolean',
|
||||
], [], [
|
||||
'fee_id' => 'Nami-Beitrag',
|
||||
'for_promise' => 'Für Versprechen benutzen',
|
||||
]);
|
||||
|
||||
$children = $request->validate([
|
||||
|
@ -73,9 +76,12 @@ class SubscriptionController extends Controller
|
|||
{
|
||||
$subscriptionParams = $request->validate([
|
||||
'name' => 'required|max:255',
|
||||
'split' => 'present|boolean',
|
||||
'fee_id' => 'required|exists:fees,id',
|
||||
'for_promise' => 'present|boolean',
|
||||
], [], [
|
||||
'fee_id' => 'Nami-Beitrag',
|
||||
'for_promise' => 'Für Versprechen benutzen',
|
||||
]);
|
||||
$subscription->update($subscriptionParams);
|
||||
$children = $request->validate([
|
||||
|
|
|
@ -28,7 +28,9 @@ class SubscriptionResource extends JsonResource
|
|||
'fee_name' => $this->fee->name,
|
||||
'amount_human' => number_format($this->getAmount() / 100, 2, ',', '.').' €',
|
||||
'amount' => $this->getAmount(),
|
||||
'split' => $this->split,
|
||||
'children' => SubscriptionChildResource::collection($this->whenLoaded('children')),
|
||||
'for_promise' => $this->for_promise,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Pdf;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Invoice\DocumentFactory;
|
||||
use App\Invoice\Queries\SingleMemberQuery;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class MemberPdfController extends Controller
|
||||
{
|
||||
/**
|
||||
* @return Response|Responsable
|
||||
*/
|
||||
public function __invoke(Request $request, Member $member)
|
||||
{
|
||||
$invoice = app(DocumentFactory::class)->singleInvoice($request->type, new SingleMemberQuery($member));
|
||||
|
||||
return null === $invoice
|
||||
? response()->noContent()
|
||||
: Tex::compile($invoice);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Invoice\Models;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Tests\Feature\Invoice\ReceiverRequestFactory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Invoice>
|
||||
*/
|
||||
class InvoiceFactory extends Factory
|
||||
{
|
||||
protected $model = Invoice::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'greeting' => $this->faker->words(4, true),
|
||||
'to' => ReceiverRequestFactory::new()->create(),
|
||||
'status' => InvoiceStatus::NEW->value,
|
||||
'via' => BillKind::POST->value,
|
||||
'usage' => $this->faker->words(4, true),
|
||||
'mail_email' => $this->faker->safeEmail(),
|
||||
];
|
||||
}
|
||||
|
||||
public function to(ReceiverRequestFactory $to): self
|
||||
{
|
||||
return $this->state(['to' => $to->create()]);
|
||||
}
|
||||
|
||||
public function sentAt(Carbon $sentAt): self
|
||||
{
|
||||
return $this->state(['sent_at' => $sentAt]);
|
||||
}
|
||||
|
||||
public function status(InvoiceStatus $status): self
|
||||
{
|
||||
return $this->state(['status' => $status->value]);
|
||||
}
|
||||
|
||||
public function via(BillKind $via): self
|
||||
{
|
||||
return $this->state(['via' => $via->value]);
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Invoice\Models;
|
||||
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<InvoicePosition>
|
||||
*/
|
||||
class InvoicePositionFactory extends Factory
|
||||
{
|
||||
protected $model = InvoicePosition::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'description' => $this->faker->words(4, true),
|
||||
'price' => $this->faker->numberBetween(1000, 2000),
|
||||
];
|
||||
}
|
||||
|
||||
public function description(string $description): self
|
||||
{
|
||||
return $this->state(['description' => $description]);
|
||||
}
|
||||
|
||||
public function price(int $price): self
|
||||
{
|
||||
return $this->state(['price' => $price]);
|
||||
}
|
||||
|
||||
public function withMember(): self
|
||||
{
|
||||
return $this->state(['member_id' => Member::factory()->defaults()->create()->id]);
|
||||
}
|
||||
}
|
|
@ -8,8 +8,10 @@ use App\Group;
|
|||
use App\Invoice\BillKind;
|
||||
use App\Member\Member;
|
||||
use App\Nationality;
|
||||
use App\Payment\Payment;
|
||||
use App\Payment\Subscription;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @extends Factory<Member>
|
||||
|
@ -80,14 +82,17 @@ class MemberFactory extends Factory
|
|||
return $this->state(['nami_id' => $namiId]);
|
||||
}
|
||||
|
||||
public function sameFamilyAs(Member $member): self
|
||||
/**
|
||||
* @param array<int, callable> $payments
|
||||
*/
|
||||
public function withPayments(array $payments): self
|
||||
{
|
||||
return $this->state([
|
||||
'firstname' => $member->firstname . 'a',
|
||||
'lastname' => $member->lastname,
|
||||
'address' => $member->address,
|
||||
'zip' => $member->zip,
|
||||
'location' => $member->location,
|
||||
]);
|
||||
return $this->afterCreating(function (Model $model) use ($payments): void {
|
||||
foreach ($payments as $paymentClosure) {
|
||||
$factory = Payment::factory()->for($model);
|
||||
$factory = call_user_func($paymentClosure, $factory);
|
||||
$factory->create();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Payment;
|
||||
|
||||
use App\Fee;
|
||||
use App\Payment\Payment;
|
||||
use App\Payment\Status;
|
||||
use App\Payment\Subscription;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Tests\RequestFactories\Child;
|
||||
|
||||
/**
|
||||
* @extends Factory<Payment>
|
||||
*/
|
||||
class PaymentFactory extends Factory
|
||||
{
|
||||
protected $model = Payment::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'nr' => $this->faker->year,
|
||||
'subscription_id' => Subscription::factory()->create()->id,
|
||||
'status_id' => Status::factory()->create()->id,
|
||||
'last_remembered_at' => $this->faker->dateTime,
|
||||
];
|
||||
}
|
||||
|
||||
public function notPaid(): self
|
||||
{
|
||||
return $this->for(Status::whereName('Nicht bezahlt')->first());
|
||||
}
|
||||
|
||||
public function paid(): self
|
||||
{
|
||||
return $this->for(Status::whereName('Rechnung beglichen')->first());
|
||||
}
|
||||
|
||||
public function nr(string $nr): self
|
||||
{
|
||||
return $this->state(['nr' => $nr]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Child> $children
|
||||
* @param array<string, mixed> $state
|
||||
*/
|
||||
public function subscription(string $name, array $children, array $state = []): self
|
||||
{
|
||||
return $this->for(
|
||||
Subscription::factory()->state(['name' => $name])->for(Fee::factory())->children($children)->state($state)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Payment;
|
||||
|
||||
use App\Payment\Status;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Status>
|
||||
*/
|
||||
class StatusFactory extends Factory
|
||||
{
|
||||
public $model = Status::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->sentence,
|
||||
'is_bill' => $this->faker->boolean,
|
||||
'is_remember' => $this->faker->boolean,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ class SubscriptionFactory extends Factory
|
|||
return [
|
||||
'name' => $this->faker->word,
|
||||
'fee_id' => Fee::factory()->createOne()->id,
|
||||
'for_promise' => false,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -41,4 +42,9 @@ class SubscriptionFactory extends Factory
|
|||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function forPromise(): self
|
||||
{
|
||||
return $this->state(['for_promise' => true]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ class CreatePaymentsTable extends Migration
|
|||
$table->boolean('is_remember');
|
||||
});
|
||||
|
||||
Status::create(['name' => 'Nicht bezahlt', 'is_bill' => true, 'is_remember' => true]);
|
||||
Status::create(['name' => 'Rechnung gestellt', 'is_bill' => false, 'is_remember' => true]);
|
||||
Status::create(['name' => 'Rechnung beglichen', 'is_bill' => false, 'is_remember' => false]);
|
||||
|
||||
Schema::create('payments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nr');
|
||||
|
|
|
@ -1,32 +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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('payments', function (Blueprint $table) {
|
||||
$table->json('invoice_data')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('payments', function (Blueprint $table) {
|
||||
$table->dropColumn('invoice_data');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,122 +0,0 @@
|
|||
<?php
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('invoices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('to');
|
||||
$table->string('greeting');
|
||||
$table->string('status');
|
||||
$table->date('sent_at')->nullable();
|
||||
$table->string('via');
|
||||
$table->string('usage');
|
||||
$table->string('mail_email')->nullable();
|
||||
$table->datetime('last_remembered_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('invoice_positions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('invoice_id');
|
||||
$table->string('description');
|
||||
$table->foreignId('member_id');
|
||||
$table->unsignedBigInteger('price');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
foreach (DB::table('subscriptions')->get() as $subscription) {
|
||||
$children = DB::table('subscription_children')->where('parent_id', $subscription->id)->get();
|
||||
if ($subscription->split === 1) {
|
||||
foreach ($children as $child) {
|
||||
$newName = 'Beitrag {year} für {name} (' . $child->name . ')';
|
||||
DB::table('subscription_children')->where('id', $child->id)->update(['name' => $newName]);
|
||||
}
|
||||
} else {
|
||||
DB::table('subscription_children')->where('parent_id', $subscription->id)->delete();
|
||||
DB::table('subscription_children')->insert([
|
||||
'id' => Str::uuid()->toString(),
|
||||
'name' => 'Beitrag {year} für {name} (' . $subscription->name . ')',
|
||||
'amount' => $children->sum('amount'),
|
||||
'parent_id' => $subscription->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$paymentGroups = DB::table('payments')->where('status_id', 2)->get()->groupBy(function ($payment) {
|
||||
$member = DB::table('members')->where('id', $payment->member_id)->first();
|
||||
return $member->lastname . $member->address . $member->location . $member->zip;
|
||||
});
|
||||
|
||||
foreach ($paymentGroups as $payments) {
|
||||
$member = DB::table('members')->where('id', $payments->first()->member_id)->first();
|
||||
$invoiceId = DB::table('invoices')->insertGetId([
|
||||
'to' => json_encode([
|
||||
'name' => 'Familie ' . $member->lastname,
|
||||
'address' => $member->address,
|
||||
'zip' => $member->zip,
|
||||
'location' => $member->location,
|
||||
]),
|
||||
'greeting' => 'Liebe Familie ' . $member->lastname,
|
||||
'status' => 'Rechnung gestellt',
|
||||
'via' => BillKind::fromValue($member->bill_kind)->value,
|
||||
'usage' => 'Mitgliedsbeitrag für ' . $member->lastname,
|
||||
'mail_email' => $member->email_parents ?: $member->email,
|
||||
'last_remembered_at' => $payments->first()->last_remembered_at,
|
||||
'sent_at' => $payments->first()->last_remembered_at,
|
||||
]);
|
||||
|
||||
foreach ($payments as $payment) {
|
||||
$subscription = DB::table('subscriptions')->where('id', $payment->subscription_id)->first();
|
||||
$subscriptionChildren = DB::table('subscription_children')->where('parent_id', $subscription->id)->get();
|
||||
$paymentMember = DB::table('members')->where('id', $payment->member_id)->first();
|
||||
foreach ($subscriptionChildren as $child) {
|
||||
DB::table('invoice_positions')->insert([
|
||||
'invoice_id' => $invoiceId,
|
||||
'description' => str($child->name)->replace('{name}', $paymentMember->firstname . ' ' . $paymentMember->lastname)->replace('{year}', $payment->nr),
|
||||
'price' => $child->amount,
|
||||
'member_id' => $member->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Schema::dropIfExists('payments');
|
||||
|
||||
Schema::table('subscriptions', function (Blueprint $table) {
|
||||
$table->dropColumn('split');
|
||||
$table->dropColumn('for_promise');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('subscriptions', function (Blueprint $table) {
|
||||
$table->boolean('split')->default(false);
|
||||
$table->boolean('for_promise')->default(false);
|
||||
});
|
||||
Schema::dropIfExists('invoice_positions');
|
||||
Schema::dropIfExists('invoices');
|
||||
Schema::create('payments', function ($table) {
|
||||
$table->id();
|
||||
$table->string('nr');
|
||||
$table->integer('subscription_id');
|
||||
$table->json('invoice_data');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -30,7 +30,6 @@ services:
|
|||
REDIS_HOST: redis
|
||||
volumes:
|
||||
- ./data/storage:/app/storage/app
|
||||
- ./data/plugins:/app/plugins
|
||||
- ./data/cookies:/app/packages/laravel-nami/.cookies
|
||||
|
||||
horizon:
|
||||
|
@ -51,7 +50,6 @@ services:
|
|||
REDIS_HOST: redis
|
||||
volumes:
|
||||
- ./data/storage:/app/storage/app
|
||||
- ./data/plugins:/app/plugins
|
||||
- ./data/cookies:/app/packages/laravel-nami/.cookies
|
||||
|
||||
schedule:
|
||||
|
@ -72,7 +70,6 @@ services:
|
|||
REDIS_HOST: redis
|
||||
volumes:
|
||||
- ./data/storage:/app/storage/app
|
||||
- ./data/plugins:/app/plugins
|
||||
- ./data/cookies:/app/packages/laravel-nami/.cookies
|
||||
|
||||
db:
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.14",
|
||||
"axios": "^1.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
|
@ -1546,12 +1545,6 @@
|
|||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
@ -5172,12 +5165,6 @@
|
|||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.14",
|
||||
"axios": "^1.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit af626f5d3a14a365e97dc6437025a0b1da6b42bc
|
||||
Subproject commit c5ea29af1bb1591238bb037da93739f5bd874334
|
|
@ -1 +1 @@
|
|||
Subproject commit b4dbd7d3125aca2c16ca9f99ec81c12a46a18e3b
|
||||
Subproject commit 6f162102ef7ceca41822d18c3e694abd926f550b
|
108
phpstan.neon
108
phpstan.neon
|
@ -4,9 +4,6 @@ includes:
|
|||
|
||||
parameters:
|
||||
|
||||
stubFiles:
|
||||
- tests/stub/phpstan/TestResponse.stub
|
||||
|
||||
paths:
|
||||
- app
|
||||
- tests
|
||||
|
@ -116,6 +113,11 @@ parameters:
|
|||
count: 1
|
||||
path: app/Member/Member.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Member\\\\Member\\:\\:createPayment\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: app/Member/Member.php
|
||||
|
||||
-
|
||||
message: "#^Unsafe usage of new static\\(\\)\\.$#"
|
||||
count: 1
|
||||
|
@ -131,6 +133,11 @@ parameters:
|
|||
count: 1
|
||||
path: app/Membership/MembershipResource.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Payment\\\\PaymentResource\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: app/Payment/PaymentResource.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Payment\\\\SubscriptionResource\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
|
@ -171,6 +178,11 @@ parameters:
|
|||
count: 1
|
||||
path: database/factories/NationalityFactory.php
|
||||
|
||||
-
|
||||
message: "#^Method Database\\\\Factories\\\\Payment\\\\StatusFactory\\:\\:definition\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: database/factories/Payment/StatusFactory.php
|
||||
|
||||
-
|
||||
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy.*#"
|
||||
|
||||
|
@ -292,6 +304,91 @@ parameters:
|
|||
count: 1
|
||||
path: packages/laravel-nami/src/Gender.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:__construct\\(\\) has parameter \\$options with no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:__construct\\(\\) has parameter \\$title with no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:__construct\\(\\) has parameter \\$url with no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:fromHttp\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has parameter \\$options with no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has parameter \\$response with no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has parameter \\$title with no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has parameter \\$url with no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:level\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$errors has no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$options has no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$response has no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$title has no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$url has no type specified\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Unsafe usage of new static\\(\\)\\.$#"
|
||||
count: 1
|
||||
path: packages/laravel-nami/src/Logger.php
|
||||
|
||||
-
|
||||
message: "#^Method Zoomyboy\\\\LaravelNami\\\\LoginException\\:\\:setReason\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
|
@ -477,6 +574,11 @@ parameters:
|
|||
count: 1
|
||||
path: app/Contribution/ContributionFactory.php
|
||||
|
||||
-
|
||||
message: "#^Return type of call to method Illuminate\\\\Support\\\\Collection\\<int,class\\-string\\<App\\\\Invoice\\\\Invoice\\>\\>\\:\\:map\\(\\) contains unresolvable type\\.$#"
|
||||
count: 1
|
||||
path: app/Payment/ActionFactory.php
|
||||
|
||||
-
|
||||
message: "#^Return type of call to method Illuminate\\\\Support\\\\Collection\\<int,class\\-string\\<App\\\\Setting\\\\LocalSettings\\>\\>\\:\\:map\\(\\) contains unresolvable type\\.$#"
|
||||
count: 1
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512.018 512.018"><path d="M6.98 256.673c-5.504 2.027-8.341 8.128-6.336 13.653l6.699 18.432L58.35 237.75zM55.257 420.513l30.72 84.48a10.57 10.57 0 0 0 5.525 6.016 10.73 10.73 0 0 0 4.501 1.003c1.259 0 2.496-.213 3.691-.661l33.899-12.501zM511.364 348.385l-35.157-96.661-84.373 84.373 41.813-15.403c5.483-2.091 11.669.768 13.696 6.315 2.048 5.525-.789 11.669-6.315 13.696l-53.12 19.584a10.617 10.617 0 0 1-3.691.661c-4.331 0-8.427-2.667-10.005-6.976-.021-.064 0-.128-.021-.192l-89.408 89.408 220.245-81.152c5.525-2.026 8.362-8.128 6.336-13.653M508.889 173.793 338.222 3.126c-4.16-4.16-10.923-4.16-15.083 0l-320 320c-4.16 4.16-4.16 10.923 0 15.083l170.667 170.667a10.56 10.56 0 0 0 7.531 3.136c2.731 0 5.461-1.045 7.552-3.115l320-320a10.7 10.7 0 0 0 0-15.104m-384 121.771L82.222 338.23a10.716 10.716 0 0 1-15.104 0c-4.16-4.16-4.16-10.923 0-15.083l42.667-42.667c4.16-4.16 10.923-4.16 15.083 0 4.16 4.161 4.181 10.902.021 15.084m184.213 13.546c-7.552 7.552-17.813 11.179-29.227 11.179-18.859 0-40.896-9.877-59.328-28.331-13.483-13.483-22.955-29.611-26.645-45.397-4.096-17.6-.725-32.917 9.493-43.157 10.219-10.24 25.536-13.611 43.157-9.493 15.787 3.691 31.915 13.141 45.397 26.645 29.633 29.61 37.185 68.522 17.153 88.554m135.787-120.213-42.667 42.667a10.716 10.716 0 0 1-15.104 0c-4.16-4.16-4.16-10.923 0-15.083l42.667-42.667c4.16-4.16 10.923-4.16 15.083 0s4.181 10.902.021 15.083"/></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,40 +1,18 @@
|
|||
<template>
|
||||
<button v-if="$attrs.onClick" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" class="btn label" v-bind="$attrs" :class="colors[color]">
|
||||
<button class="btn label" v-bind="$attrs" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" v-if="$attrs.onClick">
|
||||
<ui-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></ui-sprite>
|
||||
<span class="hidden xl:inline">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<span class="hidden xl:inline"><slot></slot></span>
|
||||
</button>
|
||||
<i-link v-if="!$attrs.onClick && !asA" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" :href="href" class="btn label" v-bind="$attrs" :class="colors[color]">
|
||||
<i-link :href="href" class="btn label" v-bind="$attrs" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" v-else>
|
||||
<ui-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></ui-sprite>
|
||||
<span class="hidden xl:inline">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<span class="hidden xl:inline"><slot></slot></span>
|
||||
</i-link>
|
||||
<a v-if="asA" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" :href="href" target="_BLANK" class="btn label" v-bind="$attrs" :class="colors[color]">
|
||||
<ui-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></ui-sprite>
|
||||
<span class="hidden xl:inline">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {menuStore} from '../../stores/menuStore.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
asA: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
href: {
|
||||
required: false,
|
||||
default: () => '#',
|
||||
},
|
||||
icon: {},
|
||||
color: {},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
menuStore: menuStore(),
|
||||
|
@ -45,11 +23,19 @@ export default {
|
|||
},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
href: {
|
||||
required: false,
|
||||
default: () => '#',
|
||||
},
|
||||
icon: {},
|
||||
color: {},
|
||||
},
|
||||
|
||||
computed: {
|
||||
slotContent() {
|
||||
return this.$slots.default()[0].children;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6">
|
||||
<div class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full" :class="innerWidth">
|
||||
<div class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-w-xl">
|
||||
<a href="#" class="absolute top-0 right-0 mt-6 mr-6" @click.prevent="$emit('close')">
|
||||
<ui-sprite src="close" class="text-zinc-400 w-6 h-6"></ui-sprite>
|
||||
</a>
|
||||
<h3 v-if="heading" class="font-semibold text-primary-200 text-xl" v-html="heading"></h3>
|
||||
<h3 class="font-semibold text-primary-200 text-xl" v-html="heading"></h3>
|
||||
<div class="text-primary-100">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
@ -15,14 +15,7 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
heading: {
|
||||
type: String,
|
||||
default: () => '',
|
||||
},
|
||||
innerWidth: {
|
||||
default: () => 'max-w-xl',
|
||||
type: String,
|
||||
},
|
||||
heading: {},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import {ref, inject, computed, onBeforeUnmount} from 'vue';
|
||||
import {ref, computed, onBeforeUnmount} from 'vue';
|
||||
import {router} from '@inertiajs/vue3';
|
||||
import useQueueEvents from './useQueueEvents.js';
|
||||
|
||||
export function useIndex(props, siteName) {
|
||||
const axios = inject('axios');
|
||||
const {startListener, stopListener} = useQueueEvents(siteName, () => reload(false));
|
||||
const rawProps = JSON.parse(JSON.stringify(props));
|
||||
const inner = {
|
||||
|
@ -57,6 +56,20 @@ export function useIndex(props, siteName) {
|
|||
reload(true);
|
||||
}
|
||||
|
||||
function requestCallback(successMessage, failureMessage) {
|
||||
return {
|
||||
onSuccess: () => {
|
||||
this.$success(successMessage);
|
||||
reload(false);
|
||||
},
|
||||
onFailure: () => {
|
||||
this.$error(failureMessage);
|
||||
reload(false);
|
||||
},
|
||||
preserveState: true,
|
||||
};
|
||||
}
|
||||
|
||||
startListener();
|
||||
onBeforeUnmount(() => stopListener());
|
||||
|
||||
|
@ -66,12 +79,12 @@ export function useIndex(props, siteName) {
|
|||
can,
|
||||
getFilter,
|
||||
setFilter,
|
||||
requestCallback,
|
||||
meta: inner.meta,
|
||||
filterString,
|
||||
router,
|
||||
toFilterString,
|
||||
reloadPage,
|
||||
axios,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
import {computed, ref, inject, onBeforeUnmount} from 'vue';
|
||||
import {router} from '@inertiajs/vue3';
|
||||
import useQueueEvents from './useQueueEvents.js';
|
||||
|
||||
export function useIndex(props, siteName) {
|
||||
const axios = inject('axios');
|
||||
const {startListener, stopListener} = useQueueEvents(siteName, () => reload(false));
|
||||
const single = ref(null);
|
||||
const rawProps = JSON.parse(JSON.stringify(props));
|
||||
const inner = {
|
||||
data: ref(rawProps.data),
|
||||
meta: ref(rawProps.meta),
|
||||
};
|
||||
|
||||
function toFilterString(data) {
|
||||
return btoa(encodeURIComponent(JSON.stringify(data)));
|
||||
}
|
||||
|
||||
const filterString = computed(() => toFilterString(inner.meta.value.filter));
|
||||
|
||||
function reload(resetPage = true, withMeta = true, data) {
|
||||
data = {
|
||||
filter: filterString.value,
|
||||
page: resetPage ? 1 : inner.meta.value.current_page,
|
||||
...data,
|
||||
};
|
||||
|
||||
router.visit(window.location.pathname, {
|
||||
data,
|
||||
preserveState: true,
|
||||
only: ['data'],
|
||||
onSuccess: (page) => {
|
||||
inner.data.value = page.props.data.data;
|
||||
if (withMeta) {
|
||||
inner.meta.value = {
|
||||
...inner.meta.value,
|
||||
...page.props.data.meta,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function reloadPage(page) {
|
||||
reload(false, true, {page: page});
|
||||
}
|
||||
|
||||
function can(permission) {
|
||||
return inner.meta.value.can[permission];
|
||||
}
|
||||
|
||||
function create() {
|
||||
single.value = JSON.parse(JSON.stringify(inner.meta.value.default));
|
||||
}
|
||||
|
||||
function edit(model) {
|
||||
single.value = JSON.parse(JSON.stringify(model));
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
single.value.id ? await axios.patch(single.value.links.update, single.value) : await axios.post(inner.meta.value.links.store, single.value);
|
||||
reload();
|
||||
single.value = null;
|
||||
}
|
||||
|
||||
async function remove(model) {
|
||||
await axios.delete(model.links.destroy);
|
||||
reload();
|
||||
}
|
||||
|
||||
function can(permission) {
|
||||
return inner.meta.value.can[permission];
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
single.value = null;
|
||||
}
|
||||
|
||||
startListener();
|
||||
onBeforeUnmount(() => stopListener());
|
||||
|
||||
return {
|
||||
data: inner.data,
|
||||
meta: inner.meta,
|
||||
single,
|
||||
create,
|
||||
edit,
|
||||
reload,
|
||||
reloadPage,
|
||||
can,
|
||||
router,
|
||||
submit,
|
||||
remove,
|
||||
cancel,
|
||||
axios,
|
||||
};
|
||||
}
|
||||
|
||||
const indexProps = {
|
||||
data: {
|
||||
default: () => {
|
||||
return {data: [], meta: {}};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
};
|
||||
|
||||
export {indexProps};
|
|
@ -11,7 +11,6 @@
|
|||
<v-link href="/" menu="dashboard" icon="loss">Dashboard</v-link>
|
||||
<v-link href="/member" menu="member" icon="user">Mitglieder</v-link>
|
||||
<v-link v-show="hasModule('bill')" href="/subscription" menu="subscription" icon="money">Beiträge</v-link>
|
||||
<v-link v-show="hasModule('bill')" href="/invoice" menu="invoice" icon="moneypaper">Rechnungen</v-link>
|
||||
<v-link href="/contribution" menu="contribution" icon="contribution">Zuschüsse</v-link>
|
||||
<v-link href="/activity" menu="activity" icon="activity">Tätigkeiten</v-link>
|
||||
<v-link href="/group" menu="group" icon="group">Gruppen</v-link>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<page-layout>
|
||||
<form class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
|
||||
<f-text id="year" v-model="inner.year" label="Jahr" required></f-text>
|
||||
|
||||
<f-switch id="for_promise" label="Versprechen einbeziehen" v-model="inner.for_promise" size="sm"></f-switch>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Absenden</button>
|
||||
</form>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
inner: {
|
||||
for_promise: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
props: {},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.$inertia.post(`/allpayment`, this.inner);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,121 +0,0 @@
|
|||
<template>
|
||||
<page-layout>
|
||||
<template #toolbar>
|
||||
<page-toolbar-button color="primary" icon="plus" @click="create">Rechnung anlegen</page-toolbar-button>
|
||||
<page-toolbar-button color="primary" icon="plus" @click="massstore = {year: ''}">Massenrechnung anlegen</page-toolbar-button>
|
||||
<page-toolbar-button :href="meta.links.masspdf" color="primary" icon="plus" as-a>Post-Briefe abrufen</page-toolbar-button>
|
||||
</template>
|
||||
|
||||
<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" name="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>
|
||||
<ui-button class="btn-primary" @click.prevent="massstore = null">Abbrechen</ui-button>
|
||||
</section>
|
||||
</form>
|
||||
</ui-popup>
|
||||
<ui-popup v-if="deleting !== null" heading="Rechnung löschen?" @close="deleting = null">
|
||||
<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-primary" @click.prevent="deleting = null">Abbrechen</a>
|
||||
</div>
|
||||
</div>
|
||||
</ui-popup>
|
||||
<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="Empfänger" container-class="grid grid-cols-2 gap-3 col-span-full">
|
||||
<f-text id="to_name" v-model="single.to.name" name="to_name" label="Name" class="col-span-full" required></f-text>
|
||||
<f-text id="to_address" v-model="single.to.address" name="to_address" label="Adresse" class="col-span-full" required></f-text>
|
||||
<f-text id="to_zip" v-model="single.to.zip" name="to_zip" label="PLZ" required></f-text>
|
||||
<f-text id="to_location" v-model="single.to.location" name="to_location" label="Ort" required></f-text>
|
||||
<f-text id="mail_email" v-model="single.mail_email" name="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>
|
||||
<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" name="greeting" label="Anrede" required></f-text>
|
||||
<f-text id="usage" v-model="single.usage" name="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" :name="`position-description-${index}`" label="Beschreibung" required></f-text>
|
||||
<f-text :id="`position-price-${index}`" v-model="position.price" mode="area" :name="`position-price-${index}`" 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">
|
||||
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
|
||||
<ui-button class="btn-primary" @click.prevent="cancel">Abbrechen</ui-button>
|
||||
</section>
|
||||
</form>
|
||||
</ui-popup>
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
|
||||
<thead>
|
||||
<th>Empfänger</th>
|
||||
<th>Gesamtbetrag</th>
|
||||
<th>Status</th>
|
||||
<th>Gesendet am</th>
|
||||
<th>Rechnungsweg</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(invoice, index) in data" :key="index">
|
||||
<td>
|
||||
<div v-text="invoice.to.name"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div v-text="invoice.sum_human"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div v-text="invoice.status"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div v-text="invoice.sent_at_human"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div v-text="invoice.via"></div>
|
||||
</td>
|
||||
<td>
|
||||
<a v-tooltip="`Anschauen`" :href="invoice.links.pdf" target="_BLANK" class="inline-flex btn btn-info btn-sm"><ui-sprite src="eye"></ui-sprite></a>
|
||||
<a v-tooltip="`Erinnerung anschauen`" :href="invoice.links.rememberpdf" target="_BLANK" class="ml-2 inline-flex btn btn-info btn-sm"><ui-sprite src="document"></ui-sprite></a>
|
||||
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="edit(invoice)"><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 = invoice"><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>
|
||||
</div>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
|
||||
const props = defineProps(indexProps);
|
||||
var {axios, meta, data, reloadPage, create, single, edit, cancel, submit, remove} = useIndex(props.data, 'invoice');
|
||||
const massstore = ref(null);
|
||||
const deleting = ref(null);
|
||||
|
||||
async function sendMassstore() {
|
||||
await axios.post(meta.value.links['mass-store'], massstore.value);
|
||||
massstore.value = null;
|
||||
}
|
||||
</script>
|
|
@ -1,35 +0,0 @@
|
|||
<template>
|
||||
<page-header title="Zahlungen" @close="$emit('close')"> </page-header>
|
||||
|
||||
<div class="grow">
|
||||
<table class="custom-table custom-table-light custom-table-sm text-sm">
|
||||
<thead>
|
||||
<th>Beschreibung</th>
|
||||
<th>Status</th>
|
||||
<th>Beitrag</th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(position, index) in data" :key="index">
|
||||
<td v-text="position.description"></td>
|
||||
<td v-text="position.invoice.status"></td>
|
||||
<td v-text="position.price_human"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineEmits(['close']);
|
||||
import { useApiIndex } from '../../composables/useApiIndex.js';
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { data, reload } = useApiIndex(props.url, 'payment');
|
||||
|
||||
await reload();
|
||||
</script>
|
|
@ -1,25 +1,24 @@
|
|||
<template>
|
||||
<page-header title="Mitgliedschaften" @close="$emit('close')">
|
||||
<template #toolbar>
|
||||
<page-toolbar-button v-if="single === null" color="primary" icon="plus" @click.prevent="create">Neue
|
||||
Mitgliedschaft</page-toolbar-button>
|
||||
<page-toolbar-button v-if="single !== null" color="primary" icon="undo"
|
||||
@click.prevent="single = null">Zurück</page-toolbar-button>
|
||||
<page-toolbar-button v-if="single === null" color="primary" icon="plus" @click.prevent="create">Neue Mitgliedschaft</page-toolbar-button>
|
||||
<page-toolbar-button v-if="single !== null" color="primary" icon="undo" @click.prevent="single = null">Zurück</page-toolbar-button>
|
||||
</template>
|
||||
</page-header>
|
||||
|
||||
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
|
||||
<f-select id="group_id" v-model="single.group_id" name="group_id" :options="meta.groups" label="Gruppierung"
|
||||
required></f-select>
|
||||
<f-select id="activity_id" v-model="single.activity_id" name="activity_id" :options="meta.activities"
|
||||
label="Tätigkeit" required></f-select>
|
||||
<f-select v-if="single.activity_id" id="subactivity_id" :model-value="single.subactivity_id" name="subactivity_id"
|
||||
:options="meta.subactivities[single.activity_id]" label="Untertätigkeit"
|
||||
@update:modelValue="setSubactivityId(single, $event)"></f-select>
|
||||
<f-switch v-if="displayPromisedAt" id="has_promise" :model-value="single.promised_at !== null"
|
||||
label="Hat Versprechen" @update:modelValue="setPromisedAtSwitch(single, $event)"></f-switch>
|
||||
<f-text v-show="displayPromisedAt && single.promised_at !== null" id="promised_at" v-model="single.promised_at"
|
||||
type="date" label="Versprechensdatum" size="sm"></f-text>
|
||||
<f-select id="group_id" v-model="single.group_id" name="group_id" :options="meta.groups" label="Gruppierung" required></f-select>
|
||||
<f-select id="activity_id" v-model="single.activity_id" name="activity_id" :options="meta.activities" label="Tätigkeit" required></f-select>
|
||||
<f-select
|
||||
v-if="single.activity_id"
|
||||
id="subactivity_id"
|
||||
v-model="single.subactivity_id"
|
||||
name="subactivity_id"
|
||||
:options="meta.subactivities[single.activity_id]"
|
||||
label="Untertätigkeit"
|
||||
></f-select>
|
||||
<f-switch id="has_promise" :model-value="single.promised_at !== null" label="Hat Versprechen" @update:modelValue="single.promised_at = $event ? '2000-02-02' : null"></f-switch>
|
||||
<f-text v-show="single.promised_at !== null" id="promised_at" v-model="single.promised_at" type="date" label="Versprechensdatum" size="sm"></f-text>
|
||||
<button type="submit" class="btn btn-primary">Absenden</button>
|
||||
</form>
|
||||
|
||||
|
@ -39,10 +38,8 @@
|
|||
<td v-text="membership.human_date"></td>
|
||||
<td><ui-boolean-display :value="membership.is_active" dark></ui-boolean-display></td>
|
||||
<td class="flex space-x-1">
|
||||
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(membership)"><ui-sprite
|
||||
src="pencil"></ui-sprite></a>
|
||||
<a href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(membership)"><ui-sprite
|
||||
src="trash"></ui-sprite></a>
|
||||
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(membership)"><ui-sprite src="pencil"></ui-sprite></a>
|
||||
<a href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(membership)"><ui-sprite src="trash"></ui-sprite></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -50,9 +47,6 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
defineEmits(['close']);
|
||||
import {useApiIndex} from '../../composables/useApiIndex.js';
|
||||
|
||||
|
@ -64,23 +58,5 @@ const props = defineProps({
|
|||
});
|
||||
const {data, meta, reload, single, create, edit, submit, remove} = useApiIndex(props.url, 'membership');
|
||||
|
||||
function setPromisedAtSwitch(single, value) {
|
||||
single.promised_at = value ? dayjs().format('YYYY-MM-DD') : null;
|
||||
}
|
||||
|
||||
const displayPromisedAt = computed(function () {
|
||||
if (!single.value || !single.value.activity_id || !single.value.subactivity_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return meta.value.subactivities[single.value.activity_id].find((s) => s.id === single.value.subactivity_id).is_age_group;
|
||||
});
|
||||
|
||||
function setSubactivityId(single, value) {
|
||||
single.subactivity_id = value;
|
||||
|
||||
return displayPromisedAt;
|
||||
}
|
||||
|
||||
await reload();
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<page-header title="Zahlungen" @close="$emit('close')">
|
||||
<template #toolbar>
|
||||
<page-toolbar-button v-if="single === null" color="primary" icon="plus" @click.prevent="create">Neue
|
||||
Zahlung</page-toolbar-button>
|
||||
<page-toolbar-button v-if="single !== null" color="primary" icon="undo"
|
||||
@click.prevent="cancel">Zurück</page-toolbar-button>
|
||||
</template>
|
||||
</page-header>
|
||||
|
||||
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
|
||||
<f-text id="nr" v-model="single.nr" label="Jahr" required></f-text>
|
||||
<f-select id="subscription_id" v-model="single.subscription_id" name="subscription_id" :options="meta.subscriptions"
|
||||
label="Beitrag" required></f-select>
|
||||
<f-select id="status_id" v-model="single.status_id" name="status_id" :options="meta.statuses" label="Status"
|
||||
required></f-select>
|
||||
<button type="submit" class="btn btn-primary">Absenden</button>
|
||||
</form>
|
||||
|
||||
<div v-else class="grow">
|
||||
<table class="custom-table custom-table-light custom-table-sm text-sm">
|
||||
<thead>
|
||||
<th>Nr</th>
|
||||
<th>Status</th>
|
||||
<th>Beitrag</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(payment, index) in data" :key="index">
|
||||
<td v-text="payment.nr"></td>
|
||||
<td v-text="payment.status_name"></td>
|
||||
<td v-text="payment.subscription.name"></td>
|
||||
<td class="flex">
|
||||
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(payment)"><ui-sprite
|
||||
src="pencil"></ui-sprite></a>
|
||||
<button v-show="!payment.is_accepted" href="#" class="inline-flex btn btn-success btn-sm"
|
||||
@click.prevent="accept(payment)"><ui-sprite src="check"></ui-sprite></button>
|
||||
<button class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(payment)"><ui-sprite
|
||||
src="trash"></ui-sprite></button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineEmits(['close']);
|
||||
import { useApiIndex } from '../../composables/useApiIndex.js';
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { axios, data, meta, reload, cancel, single, create, edit, submit, remove } = useApiIndex(props.url, 'payment');
|
||||
|
||||
async function accept(payment) {
|
||||
await axios.patch(payment.links.update, { ...payment, status_id: 3 });
|
||||
|
||||
await reload();
|
||||
}
|
||||
|
||||
await reload();
|
||||
</script>
|
|
@ -45,7 +45,7 @@
|
|||
</ui-box>
|
||||
|
||||
<ui-box heading="Zahlungen" class="area-payments">
|
||||
<payments :value="inner.invoicePositions"></payments>
|
||||
<payments :value="inner.payments"></payments>
|
||||
</ui-box>
|
||||
|
||||
<ui-box heading="Karte" container-class="grow" class="area-map hidden xl:flex">
|
||||
|
@ -59,6 +59,7 @@
|
|||
import {defineAsyncComponent} from 'vue';
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
data: {},
|
||||
meta: {},
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
<template>
|
||||
<page-layout page-class="pb-6">
|
||||
<template #toolbar>
|
||||
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied
|
||||
anlegen</page-toolbar-button>
|
||||
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary"
|
||||
icon="invoice">Rechnungen erstellen</page-toolbar-button>
|
||||
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied anlegen</page-toolbar-button>
|
||||
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary" icon="invoice">Rechnungen erstellen</page-toolbar-button>
|
||||
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.sendpayment" color="info" icon="envelope">Rechnungen versenden</page-toolbar-button>
|
||||
</template>
|
||||
<ui-popup v-if="deleting !== null" heading="Mitglied löschen?" @close="deleting.reject()">
|
||||
<div>
|
||||
<p class="mt-4">Das Mitglied "{{ deleting.member.fullname }}" löschen?</p>
|
||||
<p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls
|
||||
entfernt.</p>
|
||||
<ui-note v-if="!deleting.member.has_nami" class="mt-5" type="warning"> Dieses Mitglied ist nicht in NaMi
|
||||
vorhanden und wird daher nur in der AdReMa gelöscht werden. </ui-note>
|
||||
<p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls entfernt.</p>
|
||||
<ui-note v-if="!deleting.member.has_nami" class="mt-5" type="warning"> Dieses Mitglied ist nicht in NaMi vorhanden und wird daher nur in der AdReMa gelöscht werden. </ui-note>
|
||||
<ui-note v-if="deleting.member.has_nami" class="mt-5" type="danger">
|
||||
Dieses Mitglied ist in NaMi vorhanden und wird daher in NaMi abgemeldet werden. Sofern
|
||||
"Datenweiterverwendung" eingeschaltet ist, wird das Mitglied auf inaktiv gesetzt.
|
||||
Dieses Mitglied ist in NaMi vorhanden und wird daher in NaMi abgemeldet werden. Sofern "Datenweiterverwendung" eingeschaltet ist, wird das Mitglied auf inaktiv gesetzt.
|
||||
</ui-note>
|
||||
<div class="grid grid-cols-2 gap-3 mt-6">
|
||||
<a href="#" class="text-center btn btn-danger" @click.prevent="deleting.resolve">Mitglied loschen</a>
|
||||
|
@ -24,22 +20,45 @@
|
|||
</div>
|
||||
</ui-popup>
|
||||
<page-filter breakpoint="xl">
|
||||
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm"
|
||||
@update:model-value="setFilter('search', $event)"></f-text>
|
||||
<f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände"
|
||||
size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch>
|
||||
<f-multipleselect id="group_ids" :options="meta.groups" :model-value="getFilter('group_ids')"
|
||||
label="Gruppierungen" size="sm" name="group_ids"
|
||||
@update:model-value="setFilter('group_ids', $event)"></f-multipleselect>
|
||||
<f-select v-show="hasModule('bill')" id="billKinds" name="billKinds" :options="meta.billKinds"
|
||||
:model-value="getFilter('bill_kind')" label="Rechnung" size="sm"
|
||||
@update:model-value="setFilter('bill_kind', $event)"></f-select>
|
||||
<f-multipleselect id="activity_ids" :options="meta.filterActivities" :model-value="getFilter('activity_ids')"
|
||||
label="Tätigkeiten" size="sm" name="activity_ids"
|
||||
@update:model-value="setFilter('activity_ids', $event)"></f-multipleselect>
|
||||
<f-multipleselect id="subactivity_ids" :options="meta.filterSubactivities"
|
||||
:model-value="getFilter('subactivity_ids')" label="Untertätigkeiten" size="sm" name="subactivity_ids"
|
||||
@update:model-value="setFilter('subactivity_ids', $event)"></f-multipleselect>
|
||||
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text>
|
||||
<f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände" size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch>
|
||||
<f-multipleselect
|
||||
id="group_ids"
|
||||
:options="meta.groups"
|
||||
:model-value="getFilter('group_ids')"
|
||||
label="Gruppierungen"
|
||||
size="sm"
|
||||
name="group_ids"
|
||||
@update:model-value="setFilter('group_ids', $event)"
|
||||
></f-multipleselect>
|
||||
<f-select
|
||||
v-show="hasModule('bill')"
|
||||
id="billKinds"
|
||||
name="billKinds"
|
||||
:options="meta.billKinds"
|
||||
:model-value="getFilter('bill_kind')"
|
||||
label="Rechnung"
|
||||
size="sm"
|
||||
@update:model-value="setFilter('bill_kind', $event)"
|
||||
></f-select>
|
||||
<f-multipleselect
|
||||
id="activity_ids"
|
||||
:options="meta.filterActivities"
|
||||
:model-value="getFilter('activity_ids')"
|
||||
label="Tätigkeiten"
|
||||
size="sm"
|
||||
name="activity_ids"
|
||||
@update:model-value="setFilter('activity_ids', $event)"
|
||||
></f-multipleselect>
|
||||
<f-multipleselect
|
||||
id="subactivity_ids"
|
||||
:options="meta.filterSubactivities"
|
||||
:model-value="getFilter('subactivity_ids')"
|
||||
label="Untertätigkeiten"
|
||||
size="sm"
|
||||
name="subactivity_ids"
|
||||
@update:model-value="setFilter('subactivity_ids', $event)"
|
||||
></f-multipleselect>
|
||||
<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>
|
||||
<span class="hidden xl:inline">Exportieren</span>
|
||||
|
@ -88,14 +107,11 @@
|
|||
<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"></tags>
|
||||
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment"
|
||||
fallback=""></ui-label>
|
||||
<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>
|
||||
<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"></ui-sprite></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>
|
||||
|
@ -105,18 +121,15 @@
|
|||
</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-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>
|
||||
<member-payments v-if="single.type === 'payment'" :url="single.model.links.payment_index" @close="closeSidebar"></member-payments>
|
||||
<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>
|
||||
|
||||
<script setup>
|
||||
import MemberInvoicePositions from './MemberInvoicePositions.vue';
|
||||
import MemberPayments from './MemberPayments.vue';
|
||||
import MemberMemberships from './MemberMemberships.vue';
|
||||
import MemberCourses from './MemberCourses.vue';
|
||||
import Tags from './Tags.vue';
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
<div>
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="hidden md:table custom-table custom-table-sm text-sm">
|
||||
<thead>
|
||||
<th>Beschreibung</th>
|
||||
<th>Nr</th>
|
||||
<th>Beitrag</th>
|
||||
<th>Status</th>
|
||||
</thead>
|
||||
<tr v-for="(position, index) in inner" :key="index">
|
||||
<td v-text="position.description"></td>
|
||||
<td v-text="position.price_human"></td>
|
||||
<td v-text="position.invoice.status"></td>
|
||||
<tr v-for="(payment, index) in inner" :key="index">
|
||||
<td v-text="payment.nr"></td>
|
||||
<td v-text="`${payment.subscription.name} (${payment.subscription.amount_human})`"></td>
|
||||
<td v-text="payment.status_name"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="md:hidden grid gap-3">
|
||||
<ui-box v-for="(position, index) in inner" :key="index" class="relative" :heading="position.description" second>
|
||||
<div class="text-xs text-gray-200" v-text="position.price_human"></div>
|
||||
<div class="text-xs text-gray-200" v-text="position.invoice.status"></div>
|
||||
<ui-box class="relative" :heading="payment.nr" v-for="(payment, index) in inner" :key="index" second>
|
||||
<div class="text-xs text-gray-200" v-text="`${payment.subscription.name} (${payment.subscription.amount_human})`"></div>
|
||||
<div class="text-xs text-gray-200" v-text="payment.status_name"></div>
|
||||
</ui-box>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -24,15 +24,16 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
inner: [],
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.inner = this.value;
|
||||
},
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
<template>
|
||||
<div class="flex space-x-1">
|
||||
<i-link v-tooltip="`Details`" :href="member.links.show" class="inline-flex btn btn-primary btn-sm"><ui-sprite
|
||||
src="eye"></ui-sprite></i-link>
|
||||
<i-link v-tooltip="`Bearbeiten`" :href="member.links.edit" class="inline-flex btn btn-warning btn-sm"><ui-sprite
|
||||
src="pencil"></ui-sprite></i-link>
|
||||
<a v-show="hasModule('bill')" v-tooltip="`Zahlungen`" href="#" class="inline-flex btn btn-info btn-sm"
|
||||
@click.prevent="$emit('sidebar', 'invoicePosition')"><ui-sprite src="money"></ui-sprite></a>
|
||||
<a v-show="hasModule('courses')" v-tooltip="`Ausbildungen`" href="#" class="inline-flex btn btn-info btn-sm"
|
||||
@click.prevent="$emit('sidebar', 'courses')"><ui-sprite src="course"></ui-sprite></a>
|
||||
<a v-tooltip="`Mitgliedschaften`" href="#" class="inline-flex btn btn-info btn-sm"
|
||||
@click.prevent="$emit('sidebar', 'membership')"><ui-sprite src="user"></ui-sprite></a>
|
||||
<a v-show="member.efz_link" v-tooltip="`EFZ Formular`" :href="member.efz_link"
|
||||
class="inline-flex btn btn-info btn-sm"><ui-sprite src="report"></ui-sprite></a>
|
||||
<a v-tooltip="`Entfernen`" href="#" class="inline-flex btn btn-danger btn-sm"
|
||||
@click.prevent="$emit('remove')"><ui-sprite src="trash"></ui-sprite></a>
|
||||
<i-link v-tooltip="`Details`" :href="member.links.show" class="inline-flex btn btn-primary btn-sm"><ui-sprite src="eye"></ui-sprite></i-link>
|
||||
<i-link v-tooltip="`Bearbeiten`" :href="member.links.edit" class="inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></i-link>
|
||||
<a v-show="hasModule('bill')" v-tooltip="`Zahlungen`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'payment')"><ui-sprite src="money"></ui-sprite></a>
|
||||
<a v-show="hasModule('courses')" v-tooltip="`Ausbildungen`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'courses')"
|
||||
><ui-sprite src="course"></ui-sprite
|
||||
></a>
|
||||
<a v-tooltip="`Mitgliedschaften`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'membership')"><ui-sprite src="user"></ui-sprite></a>
|
||||
<a v-show="member.efz_link" v-tooltip="`EFZ Formular`" :href="member.efz_link" class="inline-flex btn btn-info btn-sm"><ui-sprite src="report"></ui-sprite></a>
|
||||
<a v-tooltip="`Entfernen`" href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="$emit('remove')"><ui-sprite src="trash"></ui-sprite></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -22,6 +17,6 @@ defineProps({
|
|||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<page-layout>
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="p-6" v-for="(type, index) in types" :key="index">
|
||||
<p class="text-white" v-for="(paragraph, index) in type.text" :key="index" v-text="paragraph"></p>
|
||||
<a :href="type.link.href" target="_BLANK" class="btn btn-primary mt-3 inline-block" v-text="type.link.label"></a>
|
||||
</div>
|
||||
</div>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
inner: {},
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
types: {},
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.$inertia.post(`/allpayment`, this.inner);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -10,23 +10,21 @@
|
|||
<ui-box heading="Beitrag">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<f-text id="name" v-model="inner.name" label="Name" size="sm" required></f-text>
|
||||
<f-select id="fee_id" v-model="inner.fee_id" name="fee_id" :options="fees" label="Nami-Beitrag"
|
||||
size="sm" required></f-select>
|
||||
<f-select id="fee_id" name="fee_id" :options="fees" v-model="inner.fee_id" label="Nami-Beitrag" size="sm" required></f-select>
|
||||
<f-switch id="split" label="Rechnung aufsplitten" v-model="inner.split" size="sm"></f-switch>
|
||||
<f-switch id="for_promise" label="Für Versprechen benutzen" v-model="inner.for_promise" size="sm"></f-switch>
|
||||
</div>
|
||||
</ui-box>
|
||||
<ui-box heading="Positionen">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div v-for="(pos, index) in inner.children" :key="index" class="flex space-x-2 items-end">
|
||||
<f-text :id="`name-${index}`" v-model="pos.name" label="Name" size="sm" required></f-text>
|
||||
<f-text :id="`amount-${index}`" v-model="pos.amount" label="Beitrag" size="sm" mode="area"
|
||||
required></f-text>
|
||||
<a href="#" class="btn btn-sm btn-danger icon flex-none"
|
||||
@click.prevent="inner.children.splice(index, 1)">
|
||||
<f-text :id="`amount-${index}`" v-model="pos.amount" label="Beitrag" size="sm" mode="area" required></f-text>
|
||||
<a href="#" @click.prevent="inner.children.splice(index, 1)" class="btn btn-sm btn-danger icon flex-none">
|
||||
<ui-sprite src="trash" class="w-5 h-5"></ui-sprite>
|
||||
</a>
|
||||
</div>
|
||||
<a href="#" class="btn btn-sm flex btn-primary flex self-start mt-4"
|
||||
@click.prevent="inner.children.push({ name: '', amount: 0 })">
|
||||
<a href="#" @click.prevent="inner.children.push({name: '', amount: 0})" class="btn btn-sm flex btn-primary flex self-start mt-4">
|
||||
<ui-sprite src="plus" class="w-5 h-5"></ui-sprite>
|
||||
Position hinzufügen
|
||||
</a>
|
||||
|
@ -38,6 +36,11 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
inner: {...this.data},
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
data: {},
|
||||
|
@ -45,11 +48,6 @@ export default {
|
|||
mode: {},
|
||||
meta: {},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
inner: { ...this.data },
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@component('mail::message')
|
||||
# {{ $invoice->greeting }},
|
||||
# {{ $salutation }},
|
||||
|
||||
Im Anhang findet ihr die aktuelle Rechnung des Stammes Silva für das laufende Jahr. Bitte begleicht diese bis zum angegebenen Datum.
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
@component('mail::message')
|
||||
# {{ $invoice->greeting }},
|
||||
# {{ $salutation }},
|
||||
|
||||
Hiermit möchten wir euch an die noch ausstehenden Mitgliedsbeiträge des Stammes Silva für das laufende Jahr erinnern. Bitte begleicht diese bis zum angegebenen Datum.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
\documentclass[silvaletter,12pt]{scrlttr2}
|
||||
|
||||
\setkomavar{subject}{<<< $getSubject >>>}
|
||||
\setkomavar{subject}{<<< $subject >>>}
|
||||
\setkomavar{fromname}[<<<$settings->from>>>]{<<<$settings->from_long>>>}
|
||||
\setkomavar{frommobilephone}[Mobiltelefon: ]{<<<$settings->mobile>>>}
|
||||
\setkomavar{fromemail}[E-Mail: ]{<<<$settings->email>>>}
|
||||
|
@ -11,16 +11,17 @@
|
|||
\setkomavar{fromlogo}{\includegraphics[width=2cm]{logo.png}} % stammeslogo
|
||||
|
||||
\begin{document}
|
||||
\begin{letter}{<<< $toName >>>\\<<< $toAddress >>>\\<<< $toZip >>> <<< $toLocation >>>}
|
||||
@foreach($pages as $page)
|
||||
\begin{letter}{Familie <<< $page->familyName >>>\\<<< $page->address >>>\\<<< $page->zip >>> <<< $page->location >>>}
|
||||
\sffamily
|
||||
\gdef\TotalHT{0}
|
||||
\opening{<<< $greeting >>>,}
|
||||
\opening{Liebe Familie <<< $page->familyName >>>,}
|
||||
|
||||
Hiermit stellen wir Ihnen den aktuellen Mitgliedsbeitrag für den \usekomavar*{fromname} und die DPSG in Rechnung. Dieser setzt sich wie folgt zusammen:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{0.8\textwidth}|r}
|
||||
@foreach($positions as $desc => $price)
|
||||
@foreach($page->positions as $desc => $price)
|
||||
\product{<<< $desc >>>}{<<< $price >>>}
|
||||
@endforeach
|
||||
\hline
|
||||
|
@ -34,7 +35,7 @@
|
|||
Kontoinhaber: & <<<$settings->from_long>>> \\
|
||||
IBAN: & <<<$settings->iban>>> \\
|
||||
Bic: & <<<$settings->bic>>> \\
|
||||
Verwendungszweck: & <<<$usage>>>
|
||||
Verwendungszweck: & <<<$page->usage>>>
|
||||
\end{tabular}
|
||||
|
||||
Bitte nehmen Sie zur Kenntnis, dass der für jedes Mitglied obligatorische Versicherungsschutz über die DPSG nur dann für Ihr Kind / Ihre Kinder gilt, wenn der Mitgliedsbeitrag bezahlt wurde. Wenn dies nicht geschieht, müssen wir Ihr Kind / Ihre Kinder von allen Pfadfinderaktionen ausschließen. Dazu gehören sowohl die Gruppenstunden sowie Tagesaktionen als auch mehrtägige Lager.
|
||||
|
@ -43,5 +44,6 @@
|
|||
|
||||
\closing{Viele Grüße \\ Der Stammesvorstand}
|
||||
\end{letter}
|
||||
@endforeach
|
||||
\end{document}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
\documentclass[silvaletter,12pt]{scrlttr2}
|
||||
|
||||
\setkomavar{subject}{<<< $getSubject >>>}
|
||||
\setkomavar{subject}{<<< $subject >>>}
|
||||
\setkomavar{fromname}[<<<$settings->from>>>]{<<<$settings->from_long>>>}
|
||||
\setkomavar{frommobilephone}[Mobiltelefon: ]{<<<$settings->mobile>>>}
|
||||
\setkomavar{fromemail}[E-Mail: ]{<<<$settings->email>>>}
|
||||
|
@ -11,16 +11,17 @@
|
|||
\setkomavar{fromlogo}{\includegraphics[width=2cm]{logo.png}} % stammeslogo
|
||||
|
||||
\begin{document}
|
||||
\begin{letter}{<<< $toName >>>\\<<< $toAddress >>>\\<<< $toZip >>> <<< $toLocation >>>}
|
||||
@foreach($pages as $page)
|
||||
\begin{letter}{Familie <<< $page->familyName >>>\\<<< $page->address >>>\\<<< $page->zip >>> <<< $page->location >>>}
|
||||
\sffamily
|
||||
\gdef\TotalHT{0}
|
||||
\opening{<<< $greeting >>>,}
|
||||
\opening{Liebe Familie <<< $page->familyName >>>,}
|
||||
|
||||
Ihr Mitgliedbeitrag ist noch ausstehend. Dieser setzt sich wie folgt zusammen:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{0.8\textwidth}|r}
|
||||
@foreach($positions as $desc => $price)
|
||||
@foreach($page->positions as $desc => $price)
|
||||
\product{<<< $desc >>>}{<<< $price >>>}
|
||||
@endforeach
|
||||
\hline
|
||||
|
@ -34,7 +35,7 @@
|
|||
Kontoinhaber: & <<<$settings->from_long>>> \\
|
||||
IBAN: & <<<$settings->iban>>> \\
|
||||
Bic: & <<<$settings->bic>>> \\
|
||||
Verwendungszweck: & <<<$usage>>>
|
||||
Verwendungszweck: & <<<$page->usage>>>
|
||||
\end{tabular}
|
||||
|
||||
Bitte nehmen Sie zur Kenntnis, dass der für jedes Mitglied obligatorische Versicherungsschutz über die DPSG nur dann für Ihr Kind / Ihre Kinder gilt, wenn der Mitgliedsbeitrag bezahlt wurde. Wenn dies nicht geschieht, müssen wir Ihr Kind / Ihre Kinder von allen Pfadfinderaktionen ausschließen. Dazu gehören sowohl die Gruppenstunden sowie Tagesaktionen als auch mehrtägige Lager.
|
||||
|
@ -43,5 +44,6 @@
|
|||
|
||||
\closing{Viele Grüße \\ Der Stammesvorstand}
|
||||
\end{letter}
|
||||
@endforeach
|
||||
\end{document}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ use App\Contribution\Actions\ValidateAction as ContributionValidateAction;
|
|||
use App\Course\Actions\CourseDestroyAction;
|
||||
use App\Course\Actions\CourseIndexAction;
|
||||
use App\Course\Actions\CourseStoreAction;
|
||||
use App\Invoice\Actions\InvoiceStoreAction;
|
||||
use App\Course\Actions\CourseUpdateAction;
|
||||
use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
|
||||
use App\Efz\ShowEfzDocumentAction;
|
||||
|
@ -25,14 +24,6 @@ use App\Initialize\Actions\InitializeFormAction;
|
|||
use App\Initialize\Actions\NamiGetSearchLayerAction;
|
||||
use App\Initialize\Actions\NamiLoginCheckAction;
|
||||
use App\Initialize\Actions\NamiSearchAction;
|
||||
use App\Invoice\Actions\DisplayPdfAction;
|
||||
use App\Invoice\Actions\DisplayRememberpdfAction;
|
||||
use App\Invoice\Actions\InvoiceDestroyAction;
|
||||
use App\Invoice\Actions\InvoiceIndexAction;
|
||||
use App\Invoice\Actions\InvoiceUpdateAction;
|
||||
use App\Invoice\Actions\MassPostPdfAction;
|
||||
use App\Invoice\Actions\MassStoreAction;
|
||||
use App\Invoice\Actions\PaymentPositionIndexAction;
|
||||
use App\Maildispatcher\Actions\CreateAction;
|
||||
use App\Maildispatcher\Actions\DestroyAction;
|
||||
use App\Maildispatcher\Actions\EditAction;
|
||||
|
@ -53,7 +44,15 @@ use App\Membership\Actions\MembershipDestroyAction;
|
|||
use App\Membership\Actions\MembershipStoreAction;
|
||||
use App\Membership\Actions\MembershipUpdateAction;
|
||||
use App\Membership\Actions\StoreForGroupAction;
|
||||
use App\Payment\Actions\AllpaymentPageAction;
|
||||
use App\Payment\Actions\AllpaymentStoreAction;
|
||||
use App\Payment\Actions\IndexAction as PaymentIndexAction;
|
||||
use App\Payment\Actions\PaymentDestroyAction;
|
||||
use App\Payment\Actions\PaymentStoreAction;
|
||||
use App\Payment\Actions\PaymentUpdateAction;
|
||||
use App\Payment\SendpaymentController;
|
||||
use App\Payment\SubscriptionController;
|
||||
use App\Pdf\MemberPdfController;
|
||||
|
||||
Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void {
|
||||
Auth::routes(['register' => false]);
|
||||
|
@ -69,7 +68,13 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
Route::resource('member', MemberController::class)->except('show', 'destroy');
|
||||
Route::delete('/member/{member}', MemberDeleteAction::class);
|
||||
Route::get('/member/{member}', MemberShowAction::class)->name('member.show');
|
||||
Route::get('allpayment', AllpaymentPageAction::class)->name('allpayment.page');
|
||||
Route::post('allpayment', AllpaymentStoreAction::class)->name('allpayment.store');
|
||||
Route::resource('subscription', SubscriptionController::class);
|
||||
Route::get('/member/{member}/pdf', MemberPdfController::class)
|
||||
->name('member.singlepdf');
|
||||
Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create');
|
||||
Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf');
|
||||
Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz');
|
||||
Route::get('/member/{member}/resync', MemberResyncAction::class)->name('member.resync');
|
||||
Route::get('member-export', ExportAction::class)->name('member-export');
|
||||
|
@ -102,21 +107,11 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
// ----------------------------------- group -----------------------------------
|
||||
Route::get('/group', ListAction::class)->name('group.index');
|
||||
|
||||
// -------------------------------- allpayment ---------------------------------
|
||||
Route::post('/invoice/mass-store', MassStoreAction::class)->name('invoice.mass-store');
|
||||
|
||||
// ---------------------------------- invoice ----------------------------------
|
||||
Route::get('/invoice', InvoiceIndexAction::class)->name('invoice.index');
|
||||
Route::post('/invoice', InvoiceStoreAction::class)->name('invoice.store');
|
||||
Route::patch('/invoice/{invoice}', InvoiceUpdateAction::class)->name('invoice.update');
|
||||
Route::delete('/invoice/{invoice}', InvoiceDestroyAction::class)->name('invoice.destroy');
|
||||
Route::get('/invoice/{invoice}/pdf', DisplayPdfAction::class)->name('invoice.pdf');
|
||||
Route::get('/invoice/{invoice}/rememberpdf', DisplayRememberpdfAction::class)->name('invoice.rememberpdf');
|
||||
Route::get('/invoice/masspdf', MassPostPdfAction::class)->name('invoice.masspdf');
|
||||
|
||||
|
||||
// ----------------------------- invoice-position ------------------------------
|
||||
Route::get('/member/{member}/invoice-position', PaymentPositionIndexAction::class)->name('member.invoice-position.index');
|
||||
// ---------------------------------- payment ----------------------------------
|
||||
Route::get('/member/{member}/payment', PaymentIndexAction::class)->name('member.payment.index');
|
||||
Route::post('/member/{member}/payment', PaymentStoreAction::class)->name('member.payment.store');
|
||||
Route::patch('/payment/{payment}', PaymentUpdateAction::class)->name('payment.update');
|
||||
Route::delete('/payment/{payment}', PaymentDestroyAction::class)->name('payment.destroy');
|
||||
|
||||
// --------------------------------- membership --------------------------------
|
||||
Route::get('/member/{member}/membership', MembershipIndexAction::class)->name('member.membership.index');
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
namespace Tests\EndToEnd;
|
||||
|
||||
use App\Group;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Member\Member;
|
||||
use App\Payment\Payment;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
|
@ -59,7 +57,9 @@ class MemberIndexTest extends TestCase
|
|||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
$group = Group::factory()->create();
|
||||
Member::factory()->defaults()->for($group)
|
||||
->has(InvoicePosition::factory()->for(Invoice::factory()))
|
||||
->has(Payment::factory()->notPaid()->subscription('tollerbeitrag', [
|
||||
new Child('a', 5400),
|
||||
]))
|
||||
->create(['firstname' => '::firstname::']);
|
||||
Member::factory()->defaults()->for($group)->create(['firstname' => '::firstname::']);
|
||||
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Invoice\BillDocument;
|
||||
use App\Invoice\DocumentFactory;
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Invoice\Queries\InvoiceMemberQuery;
|
||||
use App\Invoice\Queries\SingleMemberQuery;
|
||||
use App\Invoice\RememberDocument;
|
||||
use App\Member\Member;
|
||||
use App\Payment\Payment;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\RequestFactories\Child;
|
||||
use Tests\TestCase;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class DocumentFactoryTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItDisplaysMemberInformation(): void
|
||||
{
|
||||
$member = Member::factory()
|
||||
->defaults()
|
||||
->state([
|
||||
'firstname' => '::firstname::',
|
||||
'lastname' => '::lastname::',
|
||||
'address' => '::street::',
|
||||
'zip' => '::zip::',
|
||||
'location' => '::location::',
|
||||
])
|
||||
->has(Payment::factory()->notPaid()->nr('1995')->subscription('::subName::', [
|
||||
new Child('a', 1000),
|
||||
new Child('a', 500),
|
||||
]))
|
||||
->create();
|
||||
|
||||
$invoice = app(DocumentFactory::class)->singleInvoice(BillDocument::class, $this->query($member));
|
||||
|
||||
$invoice->assertHasAllContent([
|
||||
'Rechnung',
|
||||
'15.00',
|
||||
'::subName:: 1995 für ::firstname:: ::lastname::',
|
||||
'Mitgliedsbeitrag für ::lastname::',
|
||||
'Familie ::lastname::\\\\::street::\\\\::zip:: ::location::',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testItDisplaysSplitPayments(): void
|
||||
{
|
||||
$member = Member::factory()
|
||||
->defaults()
|
||||
->state([
|
||||
'firstname' => '::firstname::',
|
||||
'lastname' => '::lastname::',
|
||||
])
|
||||
->has(Payment::factory()->notPaid()->nr('1995')->subscription('::subName::', [
|
||||
new Child('a', 1000),
|
||||
new Child('b', 500),
|
||||
], ['split' => true]))
|
||||
->create();
|
||||
|
||||
$invoice = app(DocumentFactory::class)->singleInvoice(BillDocument::class, $this->query($member));
|
||||
|
||||
$invoice->assertHasAllContent([
|
||||
'Rechnung',
|
||||
'10.00',
|
||||
'5.00',
|
||||
'::subName:: (a) 1995 für ::firstname:: ::lastname::',
|
||||
'::subName:: (b) 1995 für ::firstname:: ::lastname::',
|
||||
'Mitgliedsbeitrag für ::lastname::',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testBillSetsFilename(): void
|
||||
{
|
||||
$member = Member::factory()
|
||||
->defaults()
|
||||
->state(['lastname' => '::lastname::'])
|
||||
->has(Payment::factory()->notPaid()->nr('1995'))
|
||||
->create();
|
||||
|
||||
$invoice = app(DocumentFactory::class)->singleInvoice(BillDocument::class, $this->query($member));
|
||||
|
||||
$this->assertEquals('rechnung-fur-lastname.pdf', $invoice->compiledFilename());
|
||||
}
|
||||
|
||||
public function testRememberSetsFilename(): void
|
||||
{
|
||||
$member = Member::factory()
|
||||
->defaults()
|
||||
->state(['lastname' => '::lastname::'])
|
||||
->has(Payment::factory()->notPaid()->state(['last_remembered_at' => now()->subMonths(6)]))
|
||||
->create();
|
||||
|
||||
$invoice = app(DocumentFactory::class)->singleInvoice(RememberDocument::class, $this->query($member));
|
||||
|
||||
$this->assertEquals('zahlungserinnerung-fur-lastname.pdf', $invoice->compiledFilename());
|
||||
}
|
||||
|
||||
public function testItCreatesOneFileForFamilyMembers(): void
|
||||
{
|
||||
$firstMember = Member::factory()
|
||||
->defaults()
|
||||
->state(['firstname' => 'Max1', 'lastname' => '::lastname::', 'address' => '::address::', 'zip' => '12345', 'location' => '::location::'])
|
||||
->has(Payment::factory()->notPaid()->nr('nr1'))
|
||||
->create();
|
||||
Member::factory()
|
||||
->defaults()
|
||||
->state(['firstname' => 'Max2', 'lastname' => '::lastname::', 'address' => '::address::', 'zip' => '12345', 'location' => '::location::'])
|
||||
->has(Payment::factory()->notPaid()->nr('nr2'))
|
||||
->create();
|
||||
|
||||
$invoice = app(DocumentFactory::class)->singleInvoice(BillDocument::class, $this->query($firstMember));
|
||||
|
||||
$invoice->assertHasAllContent(['Max1', 'Max2', 'nr1', 'nr2']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testWith ["App\\Invoice\\BillDocument"]
|
||||
* ["App\\Invoice\\RememberDocument"]
|
||||
*/
|
||||
public function testItDisplaysSettings(string $type): void
|
||||
{
|
||||
InvoiceSettings::fake([
|
||||
'from_long' => 'langer Stammesname',
|
||||
'from' => 'Stammeskurz',
|
||||
'mobile' => '+49 176 55555',
|
||||
'email' => 'max@muster.de',
|
||||
'website' => 'https://example.com',
|
||||
'address' => 'Musterstr 4',
|
||||
'place' => 'Münster',
|
||||
'zip' => '12345',
|
||||
'iban' => 'DE444',
|
||||
'bic' => 'SOLSSSSS',
|
||||
]);
|
||||
$member = Member::factory()
|
||||
->defaults()
|
||||
->has(Payment::factory()->notPaid()->nr('nr2'))
|
||||
->create();
|
||||
|
||||
$invoice = app(DocumentFactory::class)->singleInvoice($type, $this->query($member));
|
||||
|
||||
$invoice->assertHasAllContent([
|
||||
'langer Stammesname',
|
||||
'Stammeskurz',
|
||||
'+49 176 55555',
|
||||
'max@muster.de',
|
||||
'https://example.com',
|
||||
'Musterstr 4',
|
||||
'Münster',
|
||||
'12345',
|
||||
'DE444',
|
||||
'SOLSSSSS',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testItGeneratesAPdf(): void
|
||||
{
|
||||
Tex::fake();
|
||||
$member = Member::factory()
|
||||
->defaults()
|
||||
->has(Payment::factory()->notPaid())
|
||||
->create(['lastname' => 'lastname']);
|
||||
$this->withoutExceptionHandling();
|
||||
$this->login()->init()->loginNami();
|
||||
|
||||
$response = $this->call('GET', "/member/{$member->id}/pdf", [
|
||||
'type' => BillDocument::class,
|
||||
]);
|
||||
|
||||
$this->assertEquals('application/pdf', $response->headers->get('content-type'));
|
||||
$this->assertEquals('inline; filename="rechnung-fur-lastname.pdf"', $response->headers->get('content-disposition'));
|
||||
}
|
||||
|
||||
private function query(Member $member): InvoiceMemberQuery
|
||||
{
|
||||
return new SingleMemberQuery($member);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvoiceDestroyActionTest extends TestCase
|
||||
{
|
||||
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItDestroysInvoice(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$invoice = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->create();
|
||||
|
||||
$this->delete(route('invoice.destroy', ['invoice' => $invoice]))->assertOk();
|
||||
$this->assertDatabaseCount('invoices', 0);
|
||||
$this->assertDatabaseCount('invoice_positions', 0);
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvoiceIndexActionTest extends TestCase
|
||||
{
|
||||
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItDisplaysInvoices(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$member = Member::factory()->defaults()->create(['firstname' => 'Aaaa', 'lastname' => 'Aaab']);
|
||||
$invoice = Invoice::factory()
|
||||
->has(InvoicePosition::factory()->price(1100)->for($member)->state(['description' => 'lala']), 'positions')
|
||||
->has(InvoicePosition::factory()->price(2200)->withMember(), 'positions')
|
||||
->to(ReceiverRequestFactory::new()->name('Familie Blabla'))
|
||||
->sentAt(now()->subDay())
|
||||
->via(BillKind::POST)
|
||||
->status(InvoiceStatus::SENT)
|
||||
->create(['usage' => 'Usa', 'mail_email' => 'a@b.de']);
|
||||
|
||||
$this->get(route('invoice.index'))
|
||||
->assertInertiaPath('data.data.0.to.name', 'Familie Blabla')
|
||||
->assertInertiaPath('data.data.0.id', $invoice->id)
|
||||
->assertInertiaPath('data.data.0.sum_human', '33,00 €')
|
||||
->assertInertiaPath('data.data.0.sent_at_human', now()->subDay()->format('d.m.Y'))
|
||||
->assertInertiaPath('data.data.0.status', 'Rechnung gestellt')
|
||||
->assertInertiaPath('data.data.0.via', 'Post')
|
||||
->assertInertiaPath('data.data.0.mail_email', 'a@b.de')
|
||||
->assertInertiaPath('data.data.0.usage', 'Usa')
|
||||
->assertInertiaPath('data.data.0.greeting', $invoice->greeting)
|
||||
->assertInertiaPath('data.data.0.positions.0.price', 1100)
|
||||
->assertInertiaPath('data.data.0.positions.0.member_id', $member->id)
|
||||
->assertInertiaPath('data.data.0.positions.0.description', 'lala')
|
||||
->assertInertiaPath('data.data.0.positions.0.id', $invoice->positions->first()->id)
|
||||
->assertInertiaPath('data.data.0.links.pdf', route('invoice.pdf', ['invoice' => $invoice]))
|
||||
->assertInertiaPath('data.data.0.links.rememberpdf', route('invoice.rememberpdf', ['invoice' => $invoice]))
|
||||
->assertInertiaPath('data.data.0.links.update', route('invoice.update', ['invoice' => $invoice]))
|
||||
->assertInertiaPath('data.data.0.links.destroy', route('invoice.destroy', ['invoice' => $invoice]))
|
||||
->assertInertiaPath('data.meta.links.mass-store', route('invoice.mass-store'))
|
||||
->assertInertiaPath('data.meta.links.store', route('invoice.store'))
|
||||
->assertInertiaPath('data.meta.links.masspdf', route('invoice.masspdf'))
|
||||
->assertInertiaPath('data.meta.vias.0', ['id' => 'E-Mail', 'name' => 'E-Mail'])
|
||||
->assertInertiaPath('data.meta.statuses.0', ['id' => 'Neu', 'name' => 'Neu'])
|
||||
->assertInertiaPath('data.meta.members.0', ['id' => $member->id, 'name' => 'Aaaa Aaab'])
|
||||
->assertInertiaPath('data.meta.default', [
|
||||
'to' => [
|
||||
'name' => '',
|
||||
'address' => '',
|
||||
'zip' => '',
|
||||
'location' => '',
|
||||
],
|
||||
'positions' => [],
|
||||
'greeting' => '',
|
||||
'status' => InvoiceStatus::NEW->value,
|
||||
'via' => null,
|
||||
'usage' => '',
|
||||
'mail_email' => '',
|
||||
])
|
||||
->assertInertiaPath('data.meta.default_position', [
|
||||
'id' => null,
|
||||
'price' => 0,
|
||||
'description' => '',
|
||||
'member_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testValuesCanBeNull(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
Invoice::factory()->create();
|
||||
|
||||
$this->get(route('invoice.index'))
|
||||
->assertInertiaPath('data.data.0.sent_at_human', '');
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Member\Member;
|
||||
use Worksome\RequestFactories\RequestFactory;
|
||||
|
||||
class InvoicePositionRequestFactory extends RequestFactory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'description' => 'Beitrag Abc',
|
||||
'price' => 3250,
|
||||
'member_id' => Member::factory()->defaults()->create()->id,
|
||||
'id' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function description(string $description): self
|
||||
{
|
||||
return $this->state(['description' => $description]);
|
||||
}
|
||||
|
||||
public function price(int $price): self
|
||||
{
|
||||
return $this->state(['price' => $price]);
|
||||
}
|
||||
|
||||
public function member(Member $member): self
|
||||
{
|
||||
return $this->state(['member_id' => $member->id]);
|
||||
}
|
||||
|
||||
public function id(int $id): self
|
||||
{
|
||||
return $this->state(['id' => $id]);
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use Worksome\RequestFactories\RequestFactory;
|
||||
|
||||
class InvoiceRequestFactory extends RequestFactory
|
||||
{
|
||||
/** @var array<int, InvoicePositionRequestFactory> */
|
||||
public $positions = [];
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'to' => ReceiverRequestFactory::new(),
|
||||
'greeting' => 'Hallo Familie',
|
||||
'status' => InvoiceStatus::NEW->value,
|
||||
'via' => BillKind::EMAIL->value,
|
||||
'positions' => [],
|
||||
'usage' => $this->faker->words(4, true),
|
||||
];
|
||||
}
|
||||
|
||||
public function to(ReceiverRequestFactory $to): self
|
||||
{
|
||||
return $this->state(['to' => $to]);
|
||||
}
|
||||
|
||||
public function status(InvoiceStatus $status): self
|
||||
{
|
||||
return $this->state(['status' => $status->value]);
|
||||
}
|
||||
|
||||
public function position(InvoicePositionRequestFactory $factory): self
|
||||
{
|
||||
$this->positions[] = $factory;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create(array $attributes = []): array
|
||||
{
|
||||
return parent::create([
|
||||
'positions' => array_map(fn ($position) => $position->create(), $this->positions),
|
||||
...$attributes,
|
||||
]);
|
||||
}
|
||||
|
||||
public function via(BillKind $via): self
|
||||
{
|
||||
return $this->state(['via' => $via->value]);
|
||||
}
|
||||
}
|
|
@ -4,16 +4,14 @@ namespace Tests\Feature\Invoice;
|
|||
|
||||
use App\Invoice\Actions\InvoiceSendAction;
|
||||
use App\Invoice\BillDocument;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\Mails\BillMail;
|
||||
use App\Invoice\Mails\RememberMail;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Invoice\RememberDocument;
|
||||
use App\Member\Member;
|
||||
use App\Payment\Payment;
|
||||
use App\Payment\PaymentMail;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\RequestFactories\Child;
|
||||
use Tests\TestCase;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
|
@ -21,72 +19,42 @@ class InvoiceSendActionTest extends TestCase
|
|||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItSendsInvoices(): void
|
||||
public Member $member;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Storage::fake('temp');
|
||||
$this->withoutExceptionHandling();
|
||||
$this->login()->loginNami();
|
||||
$this->member = Member::factory()
|
||||
->defaults()
|
||||
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [
|
||||
new Child('a', 5400),
|
||||
]))
|
||||
->emailBillKind()
|
||||
->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
|
||||
}
|
||||
|
||||
public function testItCanCreatePdfPayments(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
Artisan::call('invoice:send');
|
||||
|
||||
Mail::assertSent(PaymentMail::class, fn ($mail) => Storage::disk('temp')->path('rechnung-fur-mom.pdf') === $mail->filename && Storage::disk('temp')->exists('rechnung-fur-mom.pdf'));
|
||||
}
|
||||
|
||||
public function testItCanCompileAttachment(): void
|
||||
{
|
||||
Mail::fake();
|
||||
Tex::spy();
|
||||
Storage::fake('temp');
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
$invoice = Invoice::factory()
|
||||
->to(ReceiverRequestFactory::new()->name('Familie Muster'))
|
||||
->has(InvoicePosition::factory()->withMember(), 'positions')
|
||||
->via(BillKind::EMAIL)
|
||||
->create(['mail_email' => 'max@muster.de']);
|
||||
|
||||
InvoiceSendAction::run();
|
||||
|
||||
Mail::assertSent(BillMail::class, fn ($mail) => $mail->build() && $mail->hasTo('max@muster.de', 'Familie Muster') && Storage::disk('temp')->path('rechnung-fur-familie-muster.pdf') === $mail->filename && Storage::disk('temp')->exists('rechnung-fur-familie-muster.pdf'));
|
||||
Tex::assertCompiled(BillDocument::class, fn ($document) => 'Familie Muster' === $document->toName);
|
||||
$this->assertEquals(InvoiceStatus::SENT, $invoice->fresh()->status);
|
||||
$this->assertEquals(now()->format('Y-m-d'), $invoice->fresh()->sent_at->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public function testItRemembersInvoices(): void
|
||||
{
|
||||
Mail::fake();
|
||||
Tex::spy();
|
||||
Storage::fake('temp');
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
$invoice = Invoice::factory()
|
||||
->to(ReceiverRequestFactory::new()->name('Familie Muster'))
|
||||
->has(InvoicePosition::factory()->withMember(), 'positions')
|
||||
->via(BillKind::EMAIL)
|
||||
->status(InvoiceStatus::SENT)
|
||||
->create(['sent_at' => now()->subMonths(6), 'mail_email' => 'max@muster.de', 'last_remembered_at' => now()->subMonths(6)]);
|
||||
|
||||
InvoiceSendAction::run();
|
||||
|
||||
Mail::assertSent(RememberMail::class, fn ($mail) => $mail->build() && $mail->hasTo('max@muster.de', 'Familie Muster') && Storage::disk('temp')->path('zahlungserinnerung-fur-familie-muster.pdf') === $mail->filename && Storage::disk('temp')->exists('zahlungserinnerung-fur-familie-muster.pdf'));
|
||||
Tex::assertCompiled(RememberDocument::class, fn ($document) => 'Familie Muster' === $document->toName);
|
||||
$this->assertEquals(now()->format('Y-m-d'), $invoice->fresh()->last_remembered_at->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public function testItDoesntRememberWhenNotDue(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
Invoice::factory()
|
||||
->has(InvoicePosition::factory()->withMember(), 'positions')
|
||||
->via(BillKind::EMAIL)
|
||||
->status(InvoiceStatus::SENT)
|
||||
->create(['sent_at' => now()->subMonths(6), 'last_remembered_at' => now()->subMonth()]);
|
||||
|
||||
InvoiceSendAction::run();
|
||||
|
||||
Mail::assertNotSent(RememberMail::class);
|
||||
}
|
||||
|
||||
public function testItDoesntSendPostInvoices(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
Invoice::factory()
|
||||
->has(InvoicePosition::factory()->withMember(), 'positions')
|
||||
->via(BillKind::POST)
|
||||
->create();
|
||||
|
||||
InvoiceSendAction::run();
|
||||
|
||||
Mail::assertNotSent(BillMail::class);
|
||||
Tex::assertCompiled(BillDocument::class, fn ($document) => 'Mom' === $document->pages->first()->familyName
|
||||
&& $document->pages->first()->getPositions() === ['tollerbeitrag 1997 für Lah Mom' => '54.00']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Member\Member;
|
||||
use Generator;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvoiceStoreActionTest extends TestCase
|
||||
{
|
||||
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItCanCreateAnInvoice(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$member = Member::factory()->defaults()->create();
|
||||
|
||||
$response = $this->postJson(
|
||||
route('invoice.store'),
|
||||
InvoiceRequestFactory::new()
|
||||
->to(ReceiverRequestFactory::new()->name('Familie Blabla')->address('Musterstr 44')->zip('22222')->location('Solingen'))
|
||||
->status(InvoiceStatus::PAID)
|
||||
->via(BillKind::POST)
|
||||
->state([
|
||||
'greeting' => 'Hallo Familie',
|
||||
])
|
||||
->position(InvoicePositionRequestFactory::new()->description('Beitrag Abc')->price(3250)->member($member))
|
||||
->create(['mail_email' => 'a@b.de'])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'greeting' => 'Hallo Familie',
|
||||
'via' => BillKind::POST->value,
|
||||
'status' => InvoiceStatus::PAID->value,
|
||||
'mail_email' => 'a@b.de',
|
||||
]);
|
||||
$invoice = Invoice::firstWhere('greeting', 'Hallo Familie');
|
||||
$this->assertDatabaseHas('invoice_positions', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'member_id' => $member->id,
|
||||
'price' => 3250,
|
||||
'description' => 'Beitrag Abc',
|
||||
]);
|
||||
$this->assertEquals([
|
||||
'name' => 'Familie Blabla',
|
||||
'address' => 'Musterstr 44',
|
||||
'zip' => '22222',
|
||||
'location' => 'Solingen',
|
||||
], $invoice->to);
|
||||
}
|
||||
|
||||
public function validationDataProvider(): Generator
|
||||
{
|
||||
yield [
|
||||
['to.address' => ''],
|
||||
['to.address' => 'Adresse ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['to.name' => ''],
|
||||
['to.name' => 'Name ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['to.location' => ''],
|
||||
['to.location' => 'Ort ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['status' => ''],
|
||||
['status' => 'Status ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['status' => 'lala'],
|
||||
['status' => 'Der gewählte Wert für Status ist ungültig.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['to.zip' => ''],
|
||||
['to.zip' => 'PLZ ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['via' => ''],
|
||||
['via' => 'Rechnungsweg ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['via' => 'lala'],
|
||||
['via' => 'Der gewählte Wert für Rechnungsweg ist ungültig.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['usage' => ''],
|
||||
['usage' => 'Verwendungszweck ist erforderlich.']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
* @param array<string, string> $errors
|
||||
* @dataProvider validationDataProvider
|
||||
*/
|
||||
public function testItValidatesInput(array $input, array $errors): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
|
||||
$response = $this->postJson(
|
||||
route('invoice.store'),
|
||||
InvoiceRequestFactory::new()
|
||||
->to(ReceiverRequestFactory::new())
|
||||
->position(InvoicePositionRequestFactory::new()->member(Member::factory()->defaults()->create()))
|
||||
->via(BillKind::POST)
|
||||
->state($input)
|
||||
->create()
|
||||
);
|
||||
|
||||
$response->assertJsonValidationErrors($errors);
|
||||
}
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Member\Member;
|
||||
use Generator;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvoiceUpdateActionTest extends TestCase
|
||||
{
|
||||
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItCanUpdateAnInvoice(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$invoice = Invoice::factory()->create();
|
||||
|
||||
$this->patchJson(
|
||||
route('invoice.update', ['invoice' => $invoice]),
|
||||
InvoiceRequestFactory::new()
|
||||
->to(ReceiverRequestFactory::new()->name('Familie Blabla')->address('Musterstr 44')->zip('22222')->location('Solingen'))
|
||||
->status(InvoiceStatus::PAID)
|
||||
->via(BillKind::POST)
|
||||
->state([
|
||||
'greeting' => 'Hallo Familie',
|
||||
])
|
||||
->create()
|
||||
)->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('invoices', 1);
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'greeting' => 'Hallo Familie',
|
||||
'via' => BillKind::POST->value,
|
||||
'status' => InvoiceStatus::PAID->value,
|
||||
'id' => $invoice->id,
|
||||
]);
|
||||
$invoice = Invoice::firstWhere('greeting', 'Hallo Familie');
|
||||
$this->assertEquals([
|
||||
'name' => 'Familie Blabla',
|
||||
'address' => 'Musterstr 44',
|
||||
'zip' => '22222',
|
||||
'location' => 'Solingen',
|
||||
], $invoice->to);
|
||||
}
|
||||
|
||||
public function testItAddsAPosition(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$invoice = Invoice::factory()->create();
|
||||
|
||||
$this->patchJson(
|
||||
route('invoice.update', ['invoice' => $invoice]),
|
||||
InvoiceRequestFactory::new()
|
||||
->position(InvoicePositionRequestFactory::new())
|
||||
->position(InvoicePositionRequestFactory::new())
|
||||
->create()
|
||||
)->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('invoice_positions', 2);
|
||||
}
|
||||
|
||||
public function testItUpdatesAPosition(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$invoice = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->create();
|
||||
|
||||
$this->patchJson(
|
||||
route('invoice.update', ['invoice' => $invoice]),
|
||||
InvoiceRequestFactory::new()
|
||||
->position(InvoicePositionRequestFactory::new()->description('la')->id($invoice->positions->first()->id))
|
||||
->create()
|
||||
)->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('invoice_positions', 1);
|
||||
$this->assertDatabaseHas('invoice_positions', [
|
||||
'description' => 'la',
|
||||
'id' => $invoice->positions->first()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testItDeletesAPosition(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$invoice = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->create();
|
||||
|
||||
$this->patchJson(
|
||||
route('invoice.update', ['invoice' => $invoice]),
|
||||
InvoiceRequestFactory::new()
|
||||
->create()
|
||||
)->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('invoice_positions', 0);
|
||||
}
|
||||
|
||||
public function validationDataProvider(): Generator
|
||||
{
|
||||
yield [
|
||||
['to.address' => ''],
|
||||
['to.address' => 'Adresse ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['to.name' => ''],
|
||||
['to.name' => 'Name ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['to.location' => ''],
|
||||
['to.location' => 'Ort ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['status' => ''],
|
||||
['status' => 'Status ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['status' => 'lala'],
|
||||
['status' => 'Der gewählte Wert für Status ist ungültig.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['to.zip' => ''],
|
||||
['to.zip' => 'PLZ ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['via' => ''],
|
||||
['via' => 'Rechnungsweg ist erforderlich.']
|
||||
];
|
||||
|
||||
yield [
|
||||
['via' => 'lala'],
|
||||
['via' => 'Der gewählte Wert für Rechnungsweg ist ungültig.']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
* @param array<string, string> $errors
|
||||
* @dataProvider validationDataProvider
|
||||
*/
|
||||
public function testItValidatesInput(array $input, array $errors): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
|
||||
$response = $this->postJson(
|
||||
route('invoice.store'),
|
||||
InvoiceRequestFactory::new()
|
||||
->to(ReceiverRequestFactory::new())
|
||||
->position(InvoicePositionRequestFactory::new()->member(Member::factory()->defaults()->create()))
|
||||
->via(BillKind::POST)
|
||||
->state($input)
|
||||
->create()
|
||||
);
|
||||
|
||||
$response->assertJsonValidationErrors($errors);
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MassPostPdfActionTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItDoesntDisplayPdfWhenNoMembersFound(): void
|
||||
{
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
|
||||
$this->get(route('invoice.masspdf'))->assertStatus(204);
|
||||
}
|
||||
|
||||
public function testItDoesntDisplayPdfWhenAllInvoicesPaid(): void
|
||||
{
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
|
||||
Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->via(BillKind::POST)->status(InvoiceStatus::PAID)->create();
|
||||
|
||||
$this->get(route('invoice.masspdf'))->assertStatus(204);
|
||||
}
|
||||
|
||||
public function testItDoesntDisplayEmailBills(): void
|
||||
{
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
|
||||
Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->via(BillKind::EMAIL)->create();
|
||||
|
||||
$this->get(route('invoice.masspdf'))->assertStatus(204);
|
||||
}
|
||||
|
||||
public function testItMergesRememberAndBill(): void
|
||||
{
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
$invoice1 = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->status(InvoiceStatus::NEW)
|
||||
->via(BillKind::POST)
|
||||
->create();
|
||||
$invoice2 = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->status(InvoiceStatus::SENT)
|
||||
->via(BillKind::POST)
|
||||
->create(['sent_at' => now()->subMonths(10), 'last_remembered_at' => now()->subMonths(4)]);
|
||||
|
||||
$this->get(route('invoice.masspdf'))->assertPdfPageCount(2);
|
||||
|
||||
$this->assertEquals(InvoiceStatus::SENT, $invoice1->fresh()->status);
|
||||
$this->assertEquals(now()->format('Y-m-d'), $invoice1->fresh()->last_remembered_at->format('Y-m-d'));
|
||||
$this->assertEquals(now()->format('Y-m-d'), $invoice1->fresh()->sent_at->format('Y-m-d'));
|
||||
|
||||
$this->assertEquals(InvoiceStatus::SENT, $invoice2->fresh()->status);
|
||||
$this->assertEquals(now()->format('Y-m-d'), $invoice2->fresh()->last_remembered_at->format('Y-m-d'));
|
||||
$this->assertEquals(now()->subMonths(10)->format('Y-m-d'), $invoice2->fresh()->sent_at->format('Y-m-d'));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue