Compare commits
17 Commits
11108bdfcc
...
dev-001
| Author | SHA1 | Date | |
|---|---|---|---|
| b1c333648a | |||
| fcf41c5d13 | |||
| 2b458eccd7 | |||
| 4f4dff2edd | |||
| cd526231ed | |||
| fa886aad4d | |||
| f468814a2f | |||
| ab711109a7 | |||
| 72623df38f | |||
| 9fd6839878 | |||
| fd403f8520 | |||
| 882752472e | |||
| 87531237c7 | |||
| ee7fc637f1 | |||
| bccfc11687 | |||
| 6fc65e195c | |||
| e9ae850002 |
34
app/Casts/AmountCast.php
Normal file
34
app/Casts/AmountCast.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use App\ValueObjects\Amount;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AmountCast implements CastsAttributes
|
||||
{
|
||||
public function get(Model $model, string $key, mixed $value, array $attributes): ?Amount
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Amount((float) $value, 'Euro');
|
||||
}
|
||||
|
||||
public function set(Model $model, string $key, mixed $value, array $attributes): ?float
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof Amount) {
|
||||
return $value->getAmount();
|
||||
}
|
||||
|
||||
return (float) $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
|
||||
|
||||
class ChangeCostUnitDetailsCommand {
|
||||
private ChangeCostUnitDetailsRequest $request;
|
||||
|
||||
public function __construct(ChangeCostUnitDetailsRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute(): ChangeCostUnitDetailsResponse {
|
||||
$response = new ChangeCostUnitDetailsResponse();
|
||||
|
||||
$this->request->costUnit->distance_allowance = $this->request->distanceAllowance->getAmount();
|
||||
$this->request->costUnit->mail_on_new = $this->request->mailOnNew;
|
||||
$this->request->costUnit->billing_deadline = $this->request->billingDeadline;
|
||||
|
||||
$response->success = $this->request->costUnit->save();
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
|
||||
|
||||
use App\Models\CostUnit;
|
||||
use App\ValueObjects\Amount;
|
||||
use DateTime;
|
||||
|
||||
class ChangeCostUnitDetailsRequest {
|
||||
public CostUnit $costUnit;
|
||||
|
||||
public Amount $distanceAllowance;
|
||||
public bool $mailOnNew;
|
||||
public ?DateTime $billingDeadline;
|
||||
|
||||
public function __construct(CostUnit $costUnit, Amount $distanceAllowance, bool $mailOnNew, ?DateTime $billingDeadline = null) {
|
||||
$this->costUnit = $costUnit;
|
||||
$this->distanceAllowance = $distanceAllowance;
|
||||
$this->mailOnNew = $mailOnNew;
|
||||
$this->billingDeadline = $billingDeadline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
|
||||
|
||||
class ChangeCostUnitDetailsResponse {
|
||||
public bool $success;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
|
||||
|
||||
class ChangeCostUnitStateCommand {
|
||||
private ChangeCostUnitStateRequest $request;
|
||||
|
||||
public function __construct(ChangeCostUnitStateRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : ChangeCostUnitStateResponse {
|
||||
$response = new ChangeCostUnitStateResponse();
|
||||
|
||||
$this->request->costUnit->allow_new = $this->request->allowNew;
|
||||
$this->request->costUnit->archived = $this->request->isArchived;
|
||||
$response->success = $this->request->costUnit->save();
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
|
||||
|
||||
use App\Models\CostUnit;
|
||||
|
||||
class ChangeCostUnitStateRequest {
|
||||
public bool $allowNew;
|
||||
public bool $isArchived;
|
||||
|
||||
public CostUnit $costUnit;
|
||||
|
||||
public function __construct(CostUnit $costUnit, bool $allowNew, bool $isArchived) {
|
||||
$this->costUnit = $costUnit;
|
||||
$this->allowNew = $allowNew;
|
||||
$this->isArchived = $isArchived;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
|
||||
|
||||
class ChangeCostUnitStateResponse {
|
||||
public bool $success;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class ChangeCostUnitTreasurersCommand {
|
||||
private ChangeCostUnitTreasurersRequest $request;
|
||||
|
||||
public function __construct(ChangeCostUnitTreasurersRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : ChangeCostUnitTreasurersResponse {
|
||||
$response = new ChangeCostUnitTreasurersResponse();
|
||||
|
||||
try {
|
||||
$this->request->costUnit->resetTreasurers();
|
||||
|
||||
foreach ($this->request->treasurers as $treasurer) {
|
||||
$this->request->costUnit->treasurers()->attach($treasurer);
|
||||
}
|
||||
|
||||
$this->request->costUnit->save();
|
||||
} catch (\Throwable $th) {
|
||||
$response->success = false;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
|
||||
|
||||
use App\Models\CostUnit;
|
||||
|
||||
class ChangeCostUnitTreasurersRequest {
|
||||
public array $treasurers;
|
||||
public CostUnit $costUnit;
|
||||
|
||||
public function __construct(CostUnit $costUnit, array $treasurers) {
|
||||
$this->treasurers = $treasurers;
|
||||
$this->costUnit = $costUnit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
|
||||
|
||||
class ChangeCostUnitTreasurersResponse {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
|
||||
|
||||
use App\Models\CostUnit;
|
||||
|
||||
class CreateCostUnitCommand {
|
||||
private CreateCostUnitRequest $request;
|
||||
|
||||
public function __construct(CreateCostUnitRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : CreateCostUnitResponse {
|
||||
$response = new CreateCostUnitResponse();
|
||||
$costUnit = CostUnit::create([
|
||||
'name' => $this->request->name,
|
||||
'tenant' => app('tenant')->slug,
|
||||
'type' => $this->request->type,
|
||||
'billing_deadline' => $this->request->billingDeadline,
|
||||
'distance_allowance' => $this->request->distanceAllowance->getAmount(),
|
||||
'mail_on_new' => $this->request->mailOnNew,
|
||||
'allow_new' => true,
|
||||
'archived' => false,
|
||||
]);
|
||||
|
||||
if (null !== $costUnit) {
|
||||
$response->costUnit = $costUnit;
|
||||
$response->success = true;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
|
||||
|
||||
use App\Enumerations\CostUnitType;
|
||||
use App\ValueObjects\Amount;
|
||||
use DateTime;
|
||||
|
||||
class CreateCostUnitRequest {
|
||||
public string $name;
|
||||
public string $type;
|
||||
public Amount $distanceAllowance;
|
||||
public bool $mailOnNew;
|
||||
public ?DateTime $billingDeadline;
|
||||
|
||||
public function __construct(string $name, string $type, Amount $distanceAllowance, bool $mailOnNew, ?DateTime $billingDeadline = null) {
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->distanceAllowance = $distanceAllowance;
|
||||
$this->mailOnNew = $mailOnNew;
|
||||
$this->billingDeadline = $billingDeadline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
|
||||
|
||||
use App\Models\CostUnit;
|
||||
|
||||
class CreateCostUnitResponse {
|
||||
public bool $success;
|
||||
public ?CostUnit $costUnit;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
$this->costUnit = null;
|
||||
}
|
||||
}
|
||||
47
app/Domains/CostUnit/Controllers/ChangeStateController.php
Normal file
47
app/Domains/CostUnit/Controllers/ChangeStateController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Controllers;
|
||||
|
||||
use App\Domains\CostUnit\Actions\ChangeCostUnitState\ChangeCostUnitStateCommand;
|
||||
use App\Domains\CostUnit\Actions\ChangeCostUnitState\ChangeCostUnitStateRequest;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ChangeStateController extends CommonController {
|
||||
public function close(int $costUnitId) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
|
||||
$changeStatRequest = new ChangeCostUnitStateRequest($costUnit, false, false);
|
||||
return $this->changeCostUnitState($changeStatRequest, 'Der CostUnit wurde geschlossen.');
|
||||
}
|
||||
|
||||
public function open(int $costUnitId) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
|
||||
$changeStatRequest = new ChangeCostUnitStateRequest($costUnit, true, false);
|
||||
return $this->changeCostUnitState($changeStatRequest, 'Der CostUnit wurde geöffnet.');
|
||||
}
|
||||
|
||||
public function archive(int $costUnitId) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
|
||||
$changeStatRequest = new ChangeCostUnitStateRequest($costUnit, false, true);
|
||||
return $this->changeCostUnitState($changeStatRequest, 'Der CostUnit wurde archiviert.');
|
||||
}
|
||||
|
||||
private function changeCostUnitState(ChangeCostUnitStateRequest $request, string $responseMessage) : JsonResponse {
|
||||
$changeStatCommand = new ChangeCostUnitStateCommand($request);
|
||||
|
||||
if ($changeStatCommand->execute()) {
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => $responseMessage
|
||||
]);
|
||||
};
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Ein Fehler ist aufgetreten.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
app/Domains/CostUnit/Controllers/CreateController.php
Normal file
36
app/Domains/CostUnit/Controllers/CreateController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Controllers;
|
||||
|
||||
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitCommand;
|
||||
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
|
||||
use App\Enumerations\CostUnitType;
|
||||
use App\Models\CostUnit;
|
||||
use App\Providers\FlashMessageProvider;
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use App\ValueObjects\Amount;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CreateController extends CommonController{
|
||||
public function showForm() {
|
||||
$inertiaProvider = new InertiaProvider('CostUnit/Create', []);
|
||||
return $inertiaProvider->render();
|
||||
}
|
||||
|
||||
public function createCostUnitRunningJob(Request $request) : JsonResponse {
|
||||
$createCostUnitRequest = new CreateCostUnitRequest(
|
||||
$request->get('cost_unit_name'),
|
||||
CostUnitType::COST_UNIT_TYPE_RUNNING_JOB,
|
||||
Amount::fromString($request->get('distance_allowance')),
|
||||
$request->get('mailOnNew')
|
||||
);
|
||||
|
||||
$createCostUnitCommand = new CreateCostUnitCommand($createCostUnitRequest);
|
||||
$result = $createCostUnitCommand->execute();
|
||||
new FlashMessageProvider('Die laufende Tätigkeit wurde erfolgreich angelegt.', 'success');
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Controllers;
|
||||
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DistanceAllowanceController extends CommonController {
|
||||
public function __invoke(int $costUnitId) : JsonResponse {
|
||||
$distanceAllowance = 0.00;
|
||||
$costUnit = $this->costUnits->getById($costUnitId, true);
|
||||
if (null !== $costUnit) {
|
||||
$distanceAllowance = $costUnit->distance_allowance;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'distanceAllowance' => $distanceAllowance
|
||||
]);
|
||||
}
|
||||
}
|
||||
58
app/Domains/CostUnit/Controllers/EditController.php
Normal file
58
app/Domains/CostUnit/Controllers/EditController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Controllers;
|
||||
|
||||
use App\Domains\CostUnit\Actions\ChangeCostUnitDetails\ChangeCostUnitDetailsCommand;
|
||||
use App\Domains\CostUnit\Actions\ChangeCostUnitDetails\ChangeCostUnitDetailsRequest;
|
||||
use App\Resources\CostUnitResource;
|
||||
use App\Scopes\CommonController;
|
||||
use App\ValueObjects\Amount;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EditController extends CommonController{
|
||||
function __invoke(int $costUnitId) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
if (null === $costUnit) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Die Kotenstelle konnte nicht geladen werden.'
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'costUnit' => new CostUnitResource($costUnit)->toArray(request())
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $costUnitId) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
if (null === $costUnit) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Die Kotenstelle konnte nicht geladen werden.'
|
||||
]);
|
||||
}
|
||||
|
||||
$saveParams = $request->get('formData');
|
||||
$distanceAllowance = Amount::fromString($saveParams['distanceAllowance']);
|
||||
$billingDeadline = isset($saveParams['billingDeadline']) ? \DateTime::createFromFormat('Y-m-d', $saveParams['billingDeadline']) : null;
|
||||
|
||||
$request = new ChangeCostUnitDetailsRequest($costUnit, $distanceAllowance, $saveParams['mailOnNew'], $billingDeadline);
|
||||
$command = new ChangeCostUnitDetailsCommand($request);
|
||||
$result = $command->execute();
|
||||
|
||||
if (!$result->success) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Bei der Verarbeitung ist ein Fehler aufgetreten.'
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Die Kostenstellendetails wurden erfolgreich gespeichert.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
110
app/Domains/CostUnit/Controllers/ExportController.php
Normal file
110
app/Domains/CostUnit/Controllers/ExportController.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Controllers;
|
||||
|
||||
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
|
||||
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
|
||||
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceRequest;
|
||||
use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptCommand;
|
||||
use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptRequest;
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
use App\Models\Tenant;
|
||||
use App\Providers\FileWriteProvider;
|
||||
use App\Providers\InvoiceCsvFileProvider;
|
||||
use App\Providers\PainFileProvider;
|
||||
use App\Providers\WebDavProvider;
|
||||
use App\Providers\ZipArchiveFileProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ExportController extends CommonController {
|
||||
public function __invoke(int $costUnitId) {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
|
||||
$invoicesForExport = $this->invoices->getByStatus($costUnit, InvoiceStatus::INVOICE_STATUS_APPROVED, false);
|
||||
|
||||
$webdavProvider = new WebDavProvider(WebDavProvider::INVOICE_PREFIX . $this->tenant->url . '/' . $costUnit->name);
|
||||
|
||||
$painFileData = $this->painData($invoicesForExport);
|
||||
$csvData = $this->csvData($invoicesForExport);
|
||||
|
||||
$filePrefix = Tenant::getTempDirectory();
|
||||
|
||||
$painFileWriteProvider = new FileWriteProvider($filePrefix . 'abrechnungen-' . date('Y-m-d_H-i') . '-sepa.xml', $painFileData);
|
||||
$painFileWriteProvider->writeToFile();
|
||||
|
||||
$csvFileWriteProvider = new FileWriteProvider($filePrefix . 'abrechnungen-' . date('Y-m-d_H-i') . '.csv', $csvData);
|
||||
$csvFileWriteProvider->writeToFile();
|
||||
|
||||
if ($this->tenant->upload_exports) {
|
||||
$webdavProvider->uploadFile($painFileWriteProvider->fileName);
|
||||
$webdavProvider->uploadFile($csvFileWriteProvider->fileName);
|
||||
}
|
||||
|
||||
$downloadZipArchiveFiles = [
|
||||
$painFileWriteProvider->fileName,
|
||||
$csvFileWriteProvider->fileName
|
||||
];
|
||||
|
||||
foreach ($invoicesForExport as $invoice) {
|
||||
$changeStatusRequest = new ChangeStatusRequest($invoice, InvoiceStatus::INVOICE_STATUS_EXPORTED);
|
||||
$changeStatusCommand = new ChangeStatusCommand($changeStatusRequest);
|
||||
$changeStatusCommand->execute();
|
||||
|
||||
if ($this->tenant->download_exports) {
|
||||
$createInvoiceReceiptRequest = new CreateInvoiceReceiptRequest($invoice);
|
||||
$createInvoiceReceiptCommand = new CreateInvoiceReceiptCommand($createInvoiceReceiptRequest);
|
||||
$response = $createInvoiceReceiptCommand->execute();
|
||||
|
||||
$downloadZipArchiveFiles[] = $response->fileName;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->tenant->download_exports) {
|
||||
$zipFile = Tenant::getTempDirectory() . 'Abrechnungen-' . $costUnit->name . '.zip';
|
||||
$zipFileProvider = new ZipArchiveFileProvider($zipFile);
|
||||
foreach ($downloadZipArchiveFiles as $file) {
|
||||
$zipFileProvider->addFile($file);
|
||||
}
|
||||
|
||||
$zipFileProvider->create();
|
||||
foreach ($downloadZipArchiveFiles as $file) {
|
||||
Storage::delete($file);
|
||||
}
|
||||
|
||||
Storage::delete($painFileWriteProvider->fileName);
|
||||
Storage::delete($csvFileWriteProvider->fileName);
|
||||
|
||||
return response()->download(
|
||||
storage_path('app/private/' . $zipFile),
|
||||
basename($zipFile),
|
||||
['Content-Type' => 'application/zip']
|
||||
);
|
||||
}
|
||||
|
||||
Storage::delete($painFileWriteProvider->fileName);
|
||||
Storage::delete($csvFileWriteProvider->fileName);
|
||||
return response()->json([
|
||||
'message' => 'Die Abrechnungen wurden exportiert.' . PHP_EOL .'Die Belege werden asynchron auf dem Webdav-Server hinterlegt.' . PHP_EOL . PHP_EOL . 'Sollten diese in 15 Minuten nicht vollständig sein, kontaktiere den Adminbistrator.'
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
private function painData(array $invoices) : string {
|
||||
$invoicesForPainFile = [];
|
||||
foreach ($invoices as $invoice) {
|
||||
if ($invoice->contact_bank_owner !== null && $invoice->contact_bank_iban !== '' && !$invoice->donation) {
|
||||
$invoicesForPainFile[] = $invoice;
|
||||
}
|
||||
}
|
||||
|
||||
$painFileProvider = new PainFileProvider($this->tenant->account_iban, $this->tenant->account_name, $this->tenant->account_bic, $invoicesForPainFile);
|
||||
return $painFileProvider->createPainFileContent();
|
||||
}
|
||||
|
||||
public function csvData(array $invoices) : string {
|
||||
$csvDateProvider = new InvoiceCsvFileProvider($invoices);
|
||||
return $csvDateProvider->createCsvFileContent();
|
||||
}
|
||||
}
|
||||
|
||||
48
app/Domains/CostUnit/Controllers/ListController.php
Normal file
48
app/Domains/CostUnit/Controllers/ListController.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Controllers;
|
||||
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ListController extends CommonController {
|
||||
public function __invoke() {
|
||||
$inertiaProvider = new InertiaProvider('CostUnit/List', [
|
||||
'cost_unit_id' => 1
|
||||
]);
|
||||
return $inertiaProvider->render();
|
||||
}
|
||||
|
||||
public function listCurrentEvents(Request $request) : JsonResponse {
|
||||
|
||||
return response()->json([
|
||||
'cost_unit_title' => 'Aktuelle Veranstaltungen',
|
||||
'cost_units' => $this->costUnits->getCurrentEvents(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function listCurrentRunningJobs(Request $request) : JsonResponse {
|
||||
return response()->json([
|
||||
'cost_unit_title' => 'Laufende Tätigkeiten',
|
||||
'cost_units' => $this->costUnits->getRunningJobs(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function listClosedCostUnits(Request $request) : JsonResponse {
|
||||
return response()->json([
|
||||
'cost_unit_title' => 'Geschlossene Kostenstellen',
|
||||
'cost_units' => $this->costUnits->getClosedCostUnits(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function listArchivedCostUnits(Request $request) : JsonResponse {
|
||||
return response()->json([
|
||||
'cost_unit_title' => 'Archivierte Kostenstellen',
|
||||
'cost_units' => $this->costUnits->getArchivedCostUnits(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
31
app/Domains/CostUnit/Controllers/OpenController.php
Normal file
31
app/Domains/CostUnit/Controllers/OpenController.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Controllers;
|
||||
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class OpenController extends CommonController {
|
||||
public function __invoke(int $costUnitId) {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
|
||||
|
||||
$inertiaProvider = new InertiaProvider('CostUnit/Open', [
|
||||
'costUnit' => $costUnit
|
||||
]);
|
||||
return $inertiaProvider->render();
|
||||
}
|
||||
|
||||
public function listInvoices(int $costUnitId, string $invoiceStatus) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
$invoices = $this->invoices->getByStatus($costUnit, $invoiceStatus);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'costUnit' => $costUnit,
|
||||
'invoices' => $invoices,
|
||||
'endpoint' => $invoiceStatus,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\CostUnit\Controllers;
|
||||
|
||||
use App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers\ChangeCostUnitTreasurersCommand;
|
||||
use App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers\ChangeCostUnitTreasurersRequest;
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Resources\CostUnitResource;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TreasurersEditController extends CommonController {
|
||||
public function __invoke(int $costUnitId) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
if (null === $costUnit) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Die Kostenstelle konnte nicht geladen werden.'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'costUnit' => new CostUnitResource($costUnit)->toArray(request())
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $costUnitId) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId);
|
||||
if (null === $costUnit) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Die Kostenstelle konnte nicht geladen werden.'
|
||||
]);
|
||||
}
|
||||
|
||||
$changeTreasurersRequest = new ChangeCostUnitTreasurersRequest($costUnit, $request->get('selectedTreasurers'));
|
||||
$changeTreasurersCommand = new ChangeCostUnitTreasurersCommand($changeTreasurersRequest);
|
||||
if ($changeTreasurersCommand->execute()) {
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Die Schatzis wurden erfolgreich gespeichert.'
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Beim Bearbeiten der Kostenstelle ist ein Fehler aufgetreten.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
55
app/Domains/CostUnit/Routes/api.php
Normal file
55
app/Domains/CostUnit/Routes/api.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
use App\Domains\CostUnit\Controllers\ChangeStateController;
|
||||
use App\Domains\CostUnit\Controllers\CreateController;
|
||||
use App\Domains\CostUnit\Controllers\DistanceAllowanceController;
|
||||
use App\Domains\CostUnit\Controllers\EditController;
|
||||
use App\Domains\CostUnit\Controllers\ExportController;
|
||||
use App\Domains\CostUnit\Controllers\ListController;
|
||||
use App\Domains\CostUnit\Controllers\OpenController;
|
||||
use App\Domains\CostUnit\Controllers\TreasurersEditController;
|
||||
use App\Middleware\IdentifyTenant;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('api/v1')
|
||||
->group(function () {
|
||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
Route::prefix('cost-unit')->group(function () {
|
||||
|
||||
Route::get('/get-distance-allowance/{costUnitId}', DistanceAllowanceController::class);
|
||||
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/create-running-job', [CreateController::class, 'createCostUnitRunningJob']);
|
||||
|
||||
|
||||
Route::prefix('/{costUnitId}') ->group(function () {
|
||||
Route::get('/invoice-list/{invoiceStatus}', [OpenController::class, 'listInvoices']);
|
||||
|
||||
|
||||
|
||||
|
||||
Route::post('/close', [ChangeStateController::class, 'close']);
|
||||
Route::post('/open', [ChangeStateController::class, 'open']);
|
||||
Route::post('/archive', [ChangeStateController::class, 'archive']);
|
||||
|
||||
Route::get('/details', EditController::class);
|
||||
Route::post('/details', [EditController::class, 'update']);
|
||||
|
||||
Route::get('/treasurers', TreasurersEditController::class);
|
||||
Route::post('/treasurers', [TreasurersEditController::class, 'update']);
|
||||
|
||||
Route::get('/export-payouts', ExportController::class);
|
||||
});
|
||||
|
||||
|
||||
|
||||
Route::prefix('open')->group(function () {
|
||||
Route::get('/current-events', [ListController::class, 'listCurrentEvents']);
|
||||
Route::get('/current-running-jobs', [ListController::class, 'listCurrentRunningJobs']);
|
||||
Route::get('/closed-cost-units', [ListController::class, 'listClosedCostUnits']);
|
||||
Route::get('/archived-cost-units', [ListController::class, 'listArchivedCostUnits']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
39
app/Domains/CostUnit/Routes/web.php
Normal file
39
app/Domains/CostUnit/Routes/web.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
use App\Domains\CostUnit\Controllers\CreateController;
|
||||
use App\Domains\CostUnit\Controllers\ListController;
|
||||
use App\Domains\CostUnit\Controllers\OpenController;
|
||||
use App\Domains\UserManagement\Controllers\EmailVerificationController;
|
||||
use App\Domains\UserManagement\Controllers\LoginController;
|
||||
use App\Domains\UserManagement\Controllers\LogOutController;
|
||||
use App\Domains\UserManagement\Controllers\RegistrationController;
|
||||
use App\Domains\UserManagement\Controllers\ResetPasswordController;
|
||||
use App\Middleware\IdentifyTenant;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
Route::prefix('cost-unit')->group(function () {
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/create', [CreateController::class, 'showForm']);
|
||||
Route::get('/list', ListController::class);
|
||||
Route::get('/{costUnitId}/', OpenController::class);
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
Route::get('/register', [RegistrationController::class, 'loginForm']);
|
||||
Route::get('/register/verifyEmail', [EmailVerificationController::class, 'verifyEmailForm']);
|
||||
|
||||
Route::get('/reset-password', [ResetPasswordController::class, 'resetPasswordForm']);
|
||||
|
||||
route::get('/logout', LogOutController::class);
|
||||
route::post('/login', [LoginController::class, 'doLogin']);
|
||||
route::get('/login', [LoginController::class, 'loginForm']);
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
78
app/Domains/CostUnit/Views/Create.vue
Normal file
78
app/Domains/CostUnit/Views/Create.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
import { reactive, inject } from 'vue';
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
|
||||
import AmountInput from "../../../Views/Components/AmountInput.vue";
|
||||
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
const props = defineProps({
|
||||
activeUsers: Object,
|
||||
}
|
||||
)
|
||||
|
||||
const { request } = useAjax();
|
||||
|
||||
const formData = reactive({
|
||||
cost_unit_name: '',
|
||||
distance_allowance: '0,25',
|
||||
|
||||
emailAddress: '',
|
||||
mailOnNew: true
|
||||
});
|
||||
|
||||
async function save() {
|
||||
const data = await request("/api/v1/cost-unit/create-running-job", {
|
||||
method: "POST",
|
||||
body: {
|
||||
cost_unit_name: formData.cost_unit_name,
|
||||
distance_allowance: formData.distance_allowance,
|
||||
mailOnNew: formData.mailOnNew
|
||||
}
|
||||
});
|
||||
|
||||
window.location.href = '/cost-unit/list';
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Laufende Tätigkeit hinzufügen">
|
||||
<form method="POST" action="/api/v1/cost-unit/create-running-job" @submit.prevent="save">
|
||||
<input type="hidden" name="_token" :value="csrfToken" />
|
||||
|
||||
<shadowed-box style="width: 90%; margin: 20px auto; padding: 20px;">
|
||||
<p>
|
||||
Über dieses Formular können laufende Tätigkeiten angelegt werden.<br />
|
||||
Eine Kostenstelle für eine Veranstaltung wird automatisch erstellt, sobald die Veranstaltung angelegt wurde.
|
||||
</p>
|
||||
<table style="margin-top: 40px; width: 100%">
|
||||
<tr>
|
||||
<th class="width-medium pr-20">Name der laufenden Tätigkeit</th>
|
||||
<td><input type="text" v-model="formData.cost_unit_name" class="width-half-full" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="pr-20">Kilometerpauschale</th>
|
||||
<td>
|
||||
<AmountInput v-model="formData.distance_allowance" class="width-small" /> Euro / Kilometer
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<label style="display:flex;align-items:center;cursor:pointer;">
|
||||
<input type="checkbox" v-model="formData.mailOnNew" style="margin-right:8px;cursor:pointer;" />
|
||||
<span>E-Mail-Benachrichtigung bei neuen Abrechnungen</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="submit" value="Speichern" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</shadowed-box>
|
||||
</form>
|
||||
</AppLayout>
|
||||
</template>
|
||||
83
app/Domains/CostUnit/Views/Edit.vue
Normal file
83
app/Domains/CostUnit/Views/Edit.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
import { reactive, inject } from 'vue';
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
|
||||
import AmountInput from "../../../Views/Components/AmountInput.vue";
|
||||
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
|
||||
const props = defineProps({
|
||||
activeUsers: Object,
|
||||
}
|
||||
)
|
||||
|
||||
const { request } = useAjax();
|
||||
|
||||
const formData = reactive({
|
||||
cost_unit_name: '',
|
||||
distance_allowance: '0,25',
|
||||
|
||||
emailAddress: '',
|
||||
mailOnNew: true
|
||||
});
|
||||
|
||||
async function save() {
|
||||
const data = await request("/wp-json/mareike/costunits/create-new-cost-unit", {
|
||||
method: "POST",
|
||||
body: {
|
||||
mareike_nonce: _mareike_nonce(),
|
||||
cost_unit_name: formData.cost_unit_name,
|
||||
distance_allowance: formData.distance_allowance,
|
||||
email_address: formData.emailAddress,
|
||||
mailOnNew: formData.mailOnNew
|
||||
}
|
||||
});
|
||||
|
||||
toast('Die laufende Tätigkeit wurde erfolgreich angelegt.', { type: 'success' });
|
||||
window.location.href = '/cost-units';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Laufende Tätigkeit hinzufügen">
|
||||
<shadowed-box style="width: 90%; margin: 20px auto; padding: 20px;">
|
||||
<p>
|
||||
Über dieses Formular können laufende Tätigkeiten angelegt werden.<br />
|
||||
Eine Kostenstelle für eine Veranstaltung wird automatisch erstellt, sobald die Veranstaltung angelegt wurde.
|
||||
</p>
|
||||
<table style="margin-top: 40px; width: 100%">
|
||||
<tr>
|
||||
<th class="width-medium pr-20">Name der laufenden Tätigkeit</th>
|
||||
<td><input type="text" v-model="formData.cost_unit_name" class="width-half-full" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="pr-20">Kilometerpauschale</th>
|
||||
<td>
|
||||
<AmountInput v-model="formData.distance_allowance" class="width-small" /> Euro / Kilometer
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<label style="display:flex;align-items:center;cursor:pointer;">
|
||||
<input type="checkbox" v-model="formData.mailOnNew" style="margin-right:8px;cursor:pointer;" />
|
||||
<span>E-Mail-Benachrichtigung bei neuen Abrechnungen</span>
|
||||
</label>
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<span v-for="user in props.activeUsers">
|
||||
<input type="checkbox" :id="'user_' + user.id" />
|
||||
<label :for="'user_' + user.id">{{user.fullname}} ({{user.localGroup}})</label>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="button" @click="save" value="Speichern" class="mareike-button button-small button-block" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</shadowed-box>
|
||||
</AppLayout>
|
||||
</template>
|
||||
82
app/Domains/CostUnit/Views/List.vue
Normal file
82
app/Domains/CostUnit/Views/List.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import {reactive, inject, onMounted} from 'vue';
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
|
||||
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
import ListCostUnits from "./Partials/ListCostUnits.vue";
|
||||
|
||||
const props = defineProps({
|
||||
message: String,
|
||||
|
||||
data: {
|
||||
type: [Array, Object],
|
||||
default: () => []
|
||||
},
|
||||
cost_unit_id: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
invoice_id: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
// Prüfen, ob ein ?id= Parameter in der URL übergeben wurde
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const initialCostUnitId = props.cost_unit_id
|
||||
const initialInvoiceId = props.invoice_id
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: 'Aktuelle Veranstaltungen',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/current-events",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Laufende Tätigkeiten',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/current-running-jobs",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
},
|
||||
{
|
||||
title: 'Geschlossene Kostenstellen',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/closed-cost-units",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
},
|
||||
{
|
||||
title: 'Archivierte Kostenstellen',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/archived-cost-units",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (undefined !== props.message) {
|
||||
toast.success(props.message)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Kostenstellen">
|
||||
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
|
||||
<tabbed-page :tabs="tabs" :initial-tab-id="initialCostUnitId" :initial-sub-tab-id="initialInvoiceId" />
|
||||
|
||||
</shadowed-box>
|
||||
</AppLayout>
|
||||
</template>
|
||||
67
app/Domains/CostUnit/Views/Open.vue
Normal file
67
app/Domains/CostUnit/Views/Open.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import {reactive, inject, onMounted} from 'vue';
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
|
||||
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
import ListInvoices from "./Partials/ListInvoices.vue";
|
||||
|
||||
const props = defineProps({
|
||||
costUnit: Object
|
||||
})
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const initialCostUnitId = props.costUnit.id
|
||||
const initialInvoiceId = props.invoice_id
|
||||
|
||||
|
||||
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: 'Neue Abrechnungen',
|
||||
component: ListInvoices,
|
||||
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/new",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Nichtexportierte Abrechnungen',
|
||||
component: ListInvoices,
|
||||
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/approved",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
},
|
||||
{
|
||||
title: 'Exportierte Abrechnungen',
|
||||
component: ListInvoices,
|
||||
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/exported",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
},
|
||||
{
|
||||
title: 'Abgelehnte Abrechnungen',
|
||||
component: ListInvoices,
|
||||
endpoint: "/api/v1/cost-unit/" + props.costUnit.id + "/invoice-list/denied",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (undefined !== props.message) {
|
||||
toast.success(props.message)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :title="'Abrechnungen ' + props.costUnit.name">
|
||||
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
|
||||
<tabbed-page :tabs="tabs" :initial-tab-id="initialCostUnitId" :initial-sub-tab-id="initialInvoiceId" />
|
||||
|
||||
</shadowed-box>
|
||||
</AppLayout>
|
||||
</template>
|
||||
90
app/Domains/CostUnit/Views/Partials/CostUnitDetails.vue
Normal file
90
app/Domains/CostUnit/Views/Partials/CostUnitDetails.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup>
|
||||
|
||||
import {reactive, ref} from "vue";
|
||||
import Modal from "../../../../Views/Components/Modal.vue";
|
||||
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
|
||||
import AmountInput from "../../../../Views/Components/AmountInput.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}, showCostUnit: Boolean
|
||||
})
|
||||
|
||||
const mail_on_new = ref(Boolean(Number(props.data.mail_on_new)))
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const { request } = useAjax()
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
|
||||
const formData = reactive({
|
||||
billingDeadline: props.data.billingDeadline,
|
||||
mailOnNew: mail_on_new.value,
|
||||
distanceAllowance: props.data.distanceAllowanceSmall,
|
||||
});
|
||||
|
||||
async function updateCostUnit() {
|
||||
const data = await request('/api/v1/cost-unit/' + props.data.id + '/details', {
|
||||
method: "POST",
|
||||
body: {
|
||||
formData
|
||||
}
|
||||
});
|
||||
|
||||
close();
|
||||
if (data.status === 'success') {
|
||||
toast.success(data.message);
|
||||
} else {
|
||||
toast.error(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Modal
|
||||
:show="showCostUnit"
|
||||
title="Details anpassen"
|
||||
@close="emit('close')"
|
||||
>
|
||||
Kilometerpauschale:
|
||||
<amount-input v-model="formData.distanceAllowance" class="width-small" /> Euro / km
|
||||
<br /><br />
|
||||
|
||||
<span v-if="props.data.type !== 'running_job'">
|
||||
Abrechnungsschluss am:
|
||||
<input type="date" style="margin-top: 10px;" id="autoclose_date" v-model="formData.billingDeadline" />
|
||||
<br /><br />
|
||||
</span>
|
||||
|
||||
<div style="margin-top: 10px;">
|
||||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="mail_on_new"
|
||||
v-model="formData.mailOnNew"
|
||||
style="margin-right: 8px; cursor: pointer;"
|
||||
/>
|
||||
<span>E-Mail-Benachrichtigung bei neuen Abrechnungen</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<input type="button" value="Speichern" @click="updateCostUnit" />
|
||||
</Modal>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.mareike-save-button {
|
||||
background-color: #2271b1 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
</style>
|
||||
231
app/Domains/CostUnit/Views/Partials/ListCostUnits.vue
Normal file
231
app/Domains/CostUnit/Views/Partials/ListCostUnits.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<script setup>
|
||||
import {createApp, ref} from 'vue'
|
||||
import LoadingModal from "../../../../Views/Components/LoadingModal.vue";
|
||||
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
|
||||
import CostUnitDetails from "./CostUnitDetails.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
import Treasurers from "./Treasurers.vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Array, Object],
|
||||
default: () => []
|
||||
},
|
||||
|
||||
deep_jump_id: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
deep_jump_id_sub: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const showInvoiceList = ref(false)
|
||||
const invoices = ref(null)
|
||||
const current_cost_unit = ref(null)
|
||||
const showLoading = ref(false)
|
||||
const show_invoice = ref(false)
|
||||
const invoice = ref(null)
|
||||
|
||||
const show_cost_unit = ref(false)
|
||||
const showTreasurers = ref(false)
|
||||
const costUnit = ref(null)
|
||||
|
||||
const { data, loading, error, request, download } = useAjax()
|
||||
|
||||
async function costUnitDetails(costUnitId) {
|
||||
const data = await request('/api/v1/cost-unit/' + costUnitId + '/details', {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
showLoading.value = false;
|
||||
|
||||
if (data.status === 'success') {
|
||||
costUnit.value = data.costUnit
|
||||
show_cost_unit.value = true
|
||||
} else {
|
||||
toast.error(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function editTreasurers(costUnitId) {
|
||||
const data = await request('/api/v1/cost-unit/' + costUnitId + '/treasurers', {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
showLoading.value = false;
|
||||
|
||||
if (data.status === 'success') {
|
||||
costUnit.value = data.costUnit
|
||||
showTreasurers.value = true
|
||||
} else {
|
||||
toast.error(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
function loadInvoices(cost_unit_id) {
|
||||
window.location.href = '/cost-unit/' + cost_unit_id;
|
||||
}
|
||||
|
||||
async function denyNewRequests(costUnitId) {
|
||||
changeCostUnitState(costUnitId, 'close');
|
||||
}
|
||||
|
||||
|
||||
async function archiveCostUnit(costUnitId) {
|
||||
changeCostUnitState(costUnitId, 'archive');
|
||||
}
|
||||
|
||||
|
||||
async function allowNewRequests(costUnitId) {
|
||||
changeCostUnitState(costUnitId, 'open');
|
||||
}
|
||||
|
||||
|
||||
async function changeCostUnitState(costUnitId, endPoint) {
|
||||
showLoading.value = true;
|
||||
const data = await request('/api/v1/cost-unit/' + costUnitId + '/' + endPoint, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
showLoading.value = false;
|
||||
if (data.status === 'success') {
|
||||
toast.success(data.message);
|
||||
document.getElementById('costUnitBox_' + costUnitId).style.display = 'none';
|
||||
} else {
|
||||
toast.error(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function exportPayouts(costUnitId) {
|
||||
showLoading.value = true;
|
||||
|
||||
|
||||
const response = await fetch('/api/v1/core/retrieve-global-data');
|
||||
const data = await response.json();
|
||||
const exportUrl = '/api/v1/cost-unit/' + costUnitId + '/export-payouts';
|
||||
|
||||
try {
|
||||
if (data.tenant.download_exports) {
|
||||
const response = await fetch(exportUrl, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Fehler beim Export (ZIP)');
|
||||
|
||||
const blob = await response.blob();
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
console.log(response.headers.get("content-type"));
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = downloadUrl;
|
||||
a.download = "Abrechnungen-Sippenstunden.zip";
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
setTimeout(() => {
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
document.body.removeChild(a);
|
||||
}, 100);
|
||||
} else {
|
||||
const response = await request(exportUrl, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
toast.success(response.message);
|
||||
}
|
||||
showLoading.value = false;
|
||||
|
||||
} catch (err) {
|
||||
showLoading.value = false;
|
||||
toast.error('Beim Export der Abrechnungen ist ein Fehler aufgetreten.');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.data.cost_units && props.data.cost_units.length > 0 && !showInvoiceList">
|
||||
<h2>{{ props.data.cost_unit_title }}</h2>
|
||||
<span v-for="costUnit in props.data.cost_units" class="costunit-list" :id="'costUnitBox_' + costUnit.id">
|
||||
<table style="width: 100%">
|
||||
<thead>
|
||||
<tr><td colspan="5">
|
||||
{{ costUnit.name }}
|
||||
</td></tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<th>Gesamtbeitrag</th>
|
||||
<td>{{ costUnit.totalAmount }}</td>
|
||||
<th>Unbearbeitet</th>
|
||||
<td>{{ costUnit.countNewInvoices }}</td>
|
||||
<td rowspan="4" style="vertical-align: top;">
|
||||
<input v-if="!costUnit.archived" type="button" value="Abrechnungen bearbeiten" @click="loadInvoices(costUnit.id)" />
|
||||
<input v-else type="button" value="Abrechnungen einsehen" />
|
||||
<br />
|
||||
<input v-if="!costUnit.archived" type="button" @click="exportPayouts(costUnit.id)" value="Genehmigte Abrechnungen exportieren" style="margin-top: 10px;" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Spenden</th>
|
||||
<td>{{ costUnit.donatedAmount }}</td>
|
||||
|
||||
<th>Nicht exportiert</th>
|
||||
<td>{{ costUnit.countApprovedInvoices }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
|
||||
<th>Ohne Auszahlung</th>
|
||||
<td colspan="2">{{ costUnit.countDonatedInvoices }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
|
||||
<th>Abgelehnt</th>
|
||||
<td colspan="2">{{ costUnit.countDeniedInvoices }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" style="width: 100%; padding-top: 20px;">
|
||||
<strong @click="costUnitDetails(costUnit.id)" v-if="costUnit.allow_new" class="link">Details anpassen</strong>
|
||||
<strong @click="editTreasurers(costUnit.id)" v-if="!costUnit.archived" class="link">Schatzis zuweisen</strong>
|
||||
<strong @click="denyNewRequests(costUnit.id)" v-if="costUnit.allow_new" class="link" style="color: #ff0000">Neue Abrechnungen verbieten</strong>
|
||||
<strong @click="allowNewRequests(costUnit.id)" v-if="!costUnit.allow_new && !costUnit.archived" class="link" style="color: #529a30">Neue Abrechnungen erlauben</strong>
|
||||
<strong @click="archiveCostUnit(costUnit.id)" v-if="!costUnit.allow_new && !costUnit.archived" class="link" style="color: #ff0000">Veranstaltung archivieren</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</span>
|
||||
|
||||
<CostUnitDetails :data="costUnit" :showCostUnit="show_cost_unit" v-if="show_cost_unit" @close="show_cost_unit = false" />
|
||||
<Treasurers :data="costUnit" :showTreasurers="showTreasurers" v-if="showTreasurers" @closeTreasurers="showTreasurers = false" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="showInvoiceList">
|
||||
<invoices :data="invoices" :load_invoice_id="props.deep_jump_id_sub" :cost_unit_id="current_cost_unit" />
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<strong style="width: 100%; text-align: center; display: block; margin-top: 20px;">
|
||||
Es gibt keine Kostenstellen in dieser Kategorie, für die du verantwortlich bist.
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<LoadingModal :show="showLoading" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.costunit-list {
|
||||
width: 96% !important;
|
||||
}
|
||||
</style>
|
||||
80
app/Domains/CostUnit/Views/Partials/ListInvoices.vue
Normal file
80
app/Domains/CostUnit/Views/Partials/ListInvoices.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import Icon from "../../../../Views/Components/Icon.vue";
|
||||
import InvoiceDetails from "../../../Invoice/Views/Partials/invoiceDetails/InvoiceDetails.vue";
|
||||
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
|
||||
import {ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: Object
|
||||
})
|
||||
|
||||
const { request } = useAjax()
|
||||
const invoice = ref(null)
|
||||
const show_invoice = ref(false)
|
||||
const localData = ref(props.data)
|
||||
|
||||
async function openInvoiceDetails(invoiceId) {
|
||||
const url = '/api/v1/invoice/details/' + invoiceId
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'GET' })
|
||||
|
||||
const result = await response.json()
|
||||
invoice.value = result.invoice
|
||||
show_invoice.value = true
|
||||
} catch (err) {
|
||||
console.error('Error fetching invoices:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
const url = "/api/v1/cost-unit/" + props.data.costUnit.id + "/invoice-list/" + props.data.endpoint
|
||||
try {
|
||||
const response = await fetch(url, { method: 'GET' })
|
||||
if (!response.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const result = await response.json()
|
||||
localData.value = result
|
||||
} catch (err) {
|
||||
console.error('Error fetching invoices:', err)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table v-if="localData.invoices.length > 0" class="invoice-list-table">
|
||||
<tr>
|
||||
<td colspan="6">{{props.data.costUnit.name}}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="invoice in localData.invoices" :id="'invoice_' + invoice.id">
|
||||
<td>{{invoice.invoiceNumber}}</td>
|
||||
<td>{{invoice.invoiceType}}</td>
|
||||
<td>
|
||||
{{invoice.amount}}
|
||||
</td>
|
||||
<td style="width: 150px;">
|
||||
<Icon v-if="invoice.donation" name="hand-holding-dollar" style="color: #ffffff; background-color: green" />
|
||||
<Icon v-if="invoice.alreadyPaid" name="comments-dollar" style="color: #ffffff; background-color: green" />
|
||||
</td>
|
||||
<td>
|
||||
{{invoice.contactName}}<br />
|
||||
<label v-if="invoice.contactEmail !== '--'">{{invoice.contactEmail}}<br /></label>
|
||||
<label v-if="invoice.contactPhone !== '--'">{{invoice.contactPhone}}<br /></label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="button" value="Abrechnung Anzeigen" @click="openInvoiceDetails(invoice.id)" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p v-else>Es sind keine Abrechnungen in dieser Kategorie vorhanden.</p>
|
||||
|
||||
<InvoiceDetails :data="invoice" :show-invoice="show_invoice" v-if="show_invoice" @close="show_invoice = false; reload()" />
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
98
app/Domains/CostUnit/Views/Partials/Treasurers.vue
Normal file
98
app/Domains/CostUnit/Views/Partials/Treasurers.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script setup>
|
||||
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import Modal from "../../../../Views/Components/Modal.vue";
|
||||
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
|
||||
import AmountInput from "../../../../Views/Components/AmountInput.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
const selectedTreasurers = ref([])
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}, showTreasurers: Boolean
|
||||
})
|
||||
|
||||
const commonProps = reactive({
|
||||
activeUsers: [],
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/core/retrieve-global-data');
|
||||
const data = await response.json();
|
||||
Object.assign(commonProps, data);
|
||||
|
||||
selectedTreasurers.value = props.data.treasurers?.map(t => t.id) ?? []
|
||||
});
|
||||
|
||||
|
||||
|
||||
const mail_on_new = ref(Boolean(Number(props.data.mail_on_new)))
|
||||
|
||||
const emit = defineEmits(['closeTreasurers'])
|
||||
|
||||
const { request } = useAjax()
|
||||
function closeTreasurers() {
|
||||
emit('closeTreasurers')
|
||||
}
|
||||
|
||||
|
||||
const formData = reactive({
|
||||
billingDeadline: props.data.billingDeadline,
|
||||
mailOnNew: mail_on_new.value,
|
||||
distanceAllowance: props.data.distanceAllowanceSmall,
|
||||
});
|
||||
|
||||
async function updateCostUnit() {
|
||||
const data = await request('/api/v1/cost-unit/' + props.data.id + '/treasurers', {
|
||||
method: "POST",
|
||||
body: {
|
||||
selectedTreasurers: selectedTreasurers.value,
|
||||
}
|
||||
});
|
||||
|
||||
closeTreasurers();
|
||||
if (data.status === 'success') {
|
||||
toast.success(data.message);
|
||||
} else {
|
||||
toast.error(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(props.data.treasurers)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Modal
|
||||
:show="showTreasurers"
|
||||
title="Schatzis zuweisen"
|
||||
@close="emit('closeTreasurers')"
|
||||
>
|
||||
<h3>Zuständige Schatzis:</h3>
|
||||
<p v-for="user in commonProps.activeUsers">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="'user_' + user.id"
|
||||
:value="user.id"
|
||||
v-model="selectedTreasurers"
|
||||
/>
|
||||
<label :for="'user_' + user.id">{{user.fullname}}</label>
|
||||
</p>
|
||||
|
||||
<input type="button" value="Speichern" @click="updateCostUnit" />
|
||||
</Modal>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.mareike-save-button {
|
||||
background-color: #2271b1 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
</style>
|
||||
@@ -5,8 +5,10 @@ namespace App\Domains\Dashboard\Controllers;
|
||||
use App\Providers\AuthCheckProvider;
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Js;
|
||||
|
||||
class DashboardController extends CommonController {
|
||||
public function __invoke(Request $request) {
|
||||
@@ -15,18 +17,27 @@ class DashboardController extends CommonController {
|
||||
}
|
||||
|
||||
return redirect()->intended('/login');
|
||||
|
||||
dd('U');
|
||||
return $this->renderForGuest($request);
|
||||
|
||||
}
|
||||
|
||||
private function renderForLoggedInUser(Request $request) {
|
||||
$authCheckProvider = new AuthCheckProvider;
|
||||
$inertiaProvider = new InertiaProvider('Dashboard/Dashboard', ['appName' => app('tenant')->name]);
|
||||
$inertiaProvider = new InertiaProvider('Dashboard/Dashboard', [
|
||||
'myInvoices' => $this->invoices->getMyInvoicesWidget()
|
||||
|
||||
]);
|
||||
return $inertiaProvider->render();
|
||||
}
|
||||
|
||||
private function renderForGuest(Request $request) {
|
||||
}
|
||||
|
||||
public function getMyInvoices() : JsonResponse {
|
||||
$invoices = $this->invoices->getMyInvoicesWidget();
|
||||
return response()->json(['myInvoices' => $invoices]);
|
||||
}
|
||||
|
||||
public function getOpenCostUnits() : JsonResponse {
|
||||
$costUnits = $this->costUnits->listForSummary(5);
|
||||
return response()->json(['openCostUnits' => $costUnits]);
|
||||
}
|
||||
}
|
||||
|
||||
15
app/Domains/Dashboard/Routes/api.php
Normal file
15
app/Domains/Dashboard/Routes/api.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Dashboard\Controllers\DashboardController;
|
||||
use App\Middleware\IdentifyTenant;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::prefix('api/v1/dashboard')->group(function () {
|
||||
Route::get('/my-invoices', [DashboardController::class, 'getMyInvoices']);
|
||||
Route::get('/open-cost-units', [DashboardController::class, 'getOpenCostUnits']);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
1
app/Domains/Dashboard/Routes/web.php
Normal file
1
app/Domains/Dashboard/Routes/web.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -1,24 +1,31 @@
|
||||
<script setup>
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
import MyInvoices from "./Partials/Widgets/MyInvoices.vue";
|
||||
|
||||
const props = defineProps({
|
||||
navbar: Object,
|
||||
tenant: String,
|
||||
user: Object,
|
||||
currentPath: String,
|
||||
myInvoices: Object,
|
||||
})
|
||||
|
||||
|
||||
function newInvoice() {
|
||||
window.location.href = '/invoice/new';
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title='Dashboard' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
|
||||
<AppLayout title='Dashboard'>
|
||||
<diV class="dashboard-widget-container">
|
||||
<shadowed-box class="dashboard-widget-box" style="width: 60%;">
|
||||
Meine Anmeldungen
|
||||
</shadowed-box>
|
||||
|
||||
<shadowed-box class="dashboard-widget-box">
|
||||
Meine Abrechnungen
|
||||
<MyInvoices />
|
||||
<input type="button" value="Neue Abrechnung" @click="newInvoice" style="margin-top: 20px;">
|
||||
|
||||
|
||||
</shadowed-box>
|
||||
</diV>
|
||||
</AppLayout>
|
||||
@@ -36,7 +43,6 @@ const props = defineProps({
|
||||
|
||||
.dashboard-widget-box {
|
||||
flex-grow: 1; display: inline-block;
|
||||
height: 150px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
31
app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue
Normal file
31
app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive} from "vue";
|
||||
|
||||
const myInvoices = reactive({
|
||||
'myInvoices': '',
|
||||
'approvedInvoices': '',
|
||||
'deniedInvoices': '',
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/dashboard/my-invoices');
|
||||
const data = await response.json();
|
||||
Object.assign(myInvoices, data);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p v-for="invoice in myInvoices.myInvoices" class="widget-content-item">
|
||||
<a :href="'/invoice/my-invoices/' + invoice.slug" class="link">{{invoice.title}} ({{invoice.count}})</a>
|
||||
<label>
|
||||
{{invoice.amount}}
|
||||
</label>
|
||||
</p>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive} from "vue";
|
||||
|
||||
const costUnits = reactive({
|
||||
'openCostUnits': '',
|
||||
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/dashboard/open-cost-units');
|
||||
const data = await response.json();
|
||||
Object.assign(costUnits, data);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="widget-content-item" v-if="costUnits.openCostUnits.length > 0">
|
||||
<tr>
|
||||
<td style="font-weight: bold">Kostenstelle</td>
|
||||
<td style="font-weight: bold">Neu</td>
|
||||
<td style="font-weight: bold">Ang</td>
|
||||
<td style="font-weight: bold">Betrag</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="costUnit in costUnits.openCostUnits">
|
||||
<td><a :href="'/cost-unit/' + costUnit.id" class="link">{{costUnit.name}}</a></td>
|
||||
<td>{{costUnit.new_invoices_count}}</td>
|
||||
<td>{{costUnit.approved_invoices_count}}</td>
|
||||
<td>{{costUnit.totalAmount}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p v-else style="padding: 10px; font-weight: bold">Es existieren im Moment keine Abrechnugnen, um die du dich kümmern musst.</p>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
74
app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php
Normal file
74
app/Domains/Event/Actions/CreateEvent/CreateEventCommand.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\CreateEvent;
|
||||
|
||||
use App\Enumerations\EatingHabit;
|
||||
use App\Models\Event;
|
||||
use App\Models\Tenant;
|
||||
use App\RelationModels\EventEatingHabits;
|
||||
use App\RelationModels\EventLocalGroups;
|
||||
|
||||
class CreateEventCommand {
|
||||
private CreateEventRequest $request;
|
||||
public function __construct(CreateEventRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
public function execute(): CreateEventResponse {
|
||||
$response = new CreateEventResponse();
|
||||
|
||||
|
||||
|
||||
$prefix = $this->request->begin->format('Y-m_');
|
||||
if (!str_starts_with($this->request->name, $prefix)) {
|
||||
$this->request->name = $prefix . $this->request->name;
|
||||
}
|
||||
|
||||
|
||||
$event = Event::create([
|
||||
'tenant' => app('tenant')->slug,
|
||||
'name' => $this->request->name,
|
||||
'location' => $this->request->location,
|
||||
'postal_code' => $this->request->postalCode,
|
||||
'email' => $this->request->email,
|
||||
'start_date' => $this->request->begin,
|
||||
'end_date' => $this->request->end,
|
||||
'early_bird_end' => $this->request->earlyBirdEnd,
|
||||
'registration_final_end' => $this->request->registrationFinalEnd,
|
||||
'early_bird_end_amount_increase' => $this->request->earlyBirdEndAmountIncrease,
|
||||
'account_owner' => $this->request->accountOwner,
|
||||
'account_iban' => $this->request->accountIban,
|
||||
'participation_fee_type' => $this->request->participationFeeType->slug,
|
||||
'pay_per_day' => $this->request->payPerDay,
|
||||
'pay_direct' => $this->request->payDirect,
|
||||
'total_max_amount' => 0,
|
||||
'support_per_person' => 0,
|
||||
'support_flat' => 0,
|
||||
]);
|
||||
|
||||
if ($event !== null) {
|
||||
EventEatingHabits::create([
|
||||
'event_id' => $event->id,
|
||||
'eating_habit_id' => EatingHabit::where('slug', EatingHabit::EATING_HABIT_VEGAN)->first()->id,
|
||||
]);
|
||||
|
||||
EventEatingHabits::create([
|
||||
'event_id' => $event->id,
|
||||
'eating_habit_id' => EatingHabit::where('slug', EatingHabit::EATING_HABIT_VEGETARIAN)->first()->id,
|
||||
]);
|
||||
|
||||
if (app('tenant')->slug === 'lv') {
|
||||
foreach(Tenant::where(['is_active_local_group' => true])->get() as $tenant) {
|
||||
EventLocalGroups::create(['event_id' => $event->id, 'local_group_id' => $tenant->id]);
|
||||
}
|
||||
} else {
|
||||
EventLocalGroups::create(['event_id' => $event->id, 'local_group_id' => app('tenant')->id]);
|
||||
}
|
||||
|
||||
|
||||
$response->success = true;
|
||||
$response->event = $event;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
40
app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php
Normal file
40
app/Domains/Event/Actions/CreateEvent/CreateEventRequest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\CreateEvent;
|
||||
|
||||
use App\Enumerations\ParticipationFeeType;
|
||||
use DateTime;
|
||||
|
||||
class CreateEventRequest {
|
||||
public string $name;
|
||||
public string $location;
|
||||
public string $postalCode;
|
||||
public string $email;
|
||||
public DateTime $begin;
|
||||
public DateTime $end;
|
||||
public DateTime $earlyBirdEnd;
|
||||
public DateTime $registrationFinalEnd;
|
||||
public int $earlyBirdEndAmountIncrease;
|
||||
public ParticipationFeeType $participationFeeType;
|
||||
public string $accountOwner;
|
||||
public string $accountIban;
|
||||
public bool $payPerDay;
|
||||
public bool $payDirect;
|
||||
|
||||
public function __construct(string $name, string $location, string $postalCode, string $email, DateTime $begin, DateTime $end, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $earlyBirdEndAmountIncrease, ParticipationFeeType $participationFeeType, string $accountOwner, string $accountIban, bool $payPerDay, bool $payDirect) {
|
||||
$this->name = $name;
|
||||
$this->location = $location;
|
||||
$this->postalCode = $postalCode;
|
||||
$this->email = $email;
|
||||
$this->begin = $begin;
|
||||
$this->end = $end;
|
||||
$this->earlyBirdEnd = $earlyBirdEnd;
|
||||
$this->registrationFinalEnd = $registrationFinalEnd;
|
||||
$this->earlyBirdEndAmountIncrease = $earlyBirdEndAmountIncrease;
|
||||
$this->participationFeeType = $participationFeeType;
|
||||
$this->accountOwner = $accountOwner;
|
||||
$this->accountIban = $accountIban;
|
||||
$this->payPerDay = $payPerDay;
|
||||
$this->payDirect = $payDirect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\CreateEvent;
|
||||
|
||||
use App\Models\Event;
|
||||
|
||||
class CreateEventResponse {
|
||||
public bool $success;
|
||||
public ?Event $event;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
$this->event = null;
|
||||
}
|
||||
}
|
||||
18
app/Domains/Event/Actions/SetCostUnit/SetCostUnitCommand.php
Normal file
18
app/Domains/Event/Actions/SetCostUnit/SetCostUnitCommand.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\SetCostUnit;
|
||||
|
||||
class SetCostUnitCommand {
|
||||
private SetCostUnitRequest $request;
|
||||
|
||||
public function __construct(SetCostUnitRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : SetCostUnitResponse {
|
||||
$response = new SetCostUnitResponse();
|
||||
$this->request->event->cost_unit_id = $this->request->costUnit->id;
|
||||
$response->success = $this->request->event->save();
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
16
app/Domains/Event/Actions/SetCostUnit/SetCostUnitRequest.php
Normal file
16
app/Domains/Event/Actions/SetCostUnit/SetCostUnitRequest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\SetCostUnit;
|
||||
|
||||
use App\Models\CostUnit;
|
||||
use App\Models\Event;
|
||||
|
||||
class SetCostUnitRequest {
|
||||
public Event $event;
|
||||
public CostUnit $costUnit;
|
||||
|
||||
public function __construct(Event $event, CostUnit $costUnit) {
|
||||
$this->event = $event;
|
||||
$this->costUnit = $costUnit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\SetCostUnit;
|
||||
|
||||
class SetCostUnitResponse {
|
||||
public bool $success;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\SetParticipationFees;
|
||||
|
||||
use App\RelationModels\EventParticipationFee;
|
||||
|
||||
class SetParticipationFeesCommand {
|
||||
private SetParticipationFeesRequest $request;
|
||||
public function __construct(SetParticipationFeesRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function excetute() : SetParticipationFeesResponse {
|
||||
$response = new SetParticipationFeesResponse();
|
||||
$this->cleanBefore();
|
||||
|
||||
$this->request->event->participationFee1()->associate(EventParticipationFee::create([
|
||||
'tenant' => app('tenant')->slug,
|
||||
'type' => $this->request->participationFeeFirst['type'],
|
||||
'name' => $this->request->participationFeeFirst['name'],
|
||||
'description' => $this->request->participationFeeFirst['description'],
|
||||
'amount' => $this->request->participationFeeFirst['amount']->getAmount()
|
||||
]))->save();
|
||||
|
||||
if ($this->request->participationFeeSecond !== null) {
|
||||
$this->request->event->participationFee2()->associate(EventParticipationFee::create([
|
||||
'tenant' => app('tenant')->slug,
|
||||
'type' => $this->request->participationFeeSecond['type'],
|
||||
'name' => $this->request->participationFeeSecond['name'],
|
||||
'description' => $this->request->participationFeeSecond['description'],
|
||||
'amount' => $this->request->participationFeeSecond['amount']->getAmount()
|
||||
]))->save();
|
||||
}
|
||||
|
||||
if ($this->request->participationFeeThird !== null) {
|
||||
$this->request->event->participationFee3()->associate(EventParticipationFee::create([
|
||||
'tenant' => app('tenant')->slug,
|
||||
'type' => $this->request->participationFeeThird['type'],
|
||||
'name' => $this->request->participationFeeThird['name'],
|
||||
'description' => $this->request->participationFeeThird['description'],
|
||||
'amount' => $this->request->participationFeeThird['amount']->getAmount()
|
||||
]))->save();
|
||||
}
|
||||
|
||||
if ($this->request->participationFeeFourth !== null) {
|
||||
$this->request->event->participationFee4()->associate(EventParticipationFee::create([
|
||||
'tenant' => app('tenant')->slug,
|
||||
'type' => $this->request->participationFeeFourth['type'],
|
||||
'name' => $this->request->participationFeeFourth['name'],
|
||||
'description' => $this->request->participationFeeFourth['description'],
|
||||
'amount' => $this->request->participationFeeFourth['amount']->getAmount()
|
||||
]))->save();
|
||||
}
|
||||
|
||||
$this->request->event->save();
|
||||
$response->success = true;
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function cleanBefore() {
|
||||
if ($this->request->event->participationFee1()->first() !== null) {
|
||||
$this->request->event->participationFee1()->first()->delete();
|
||||
}
|
||||
|
||||
if ($this->request->event->participationFee2()->first() !== null) {
|
||||
$this->request->event->participationFee2()->first()->delete();
|
||||
}
|
||||
|
||||
if ($this->request->event->participationFee3()->first() !== null) {
|
||||
$this->request->event->participationFee3()->first()->delete();
|
||||
}
|
||||
|
||||
if ($this->request->event->participationFee4()->first() !== null) {
|
||||
$this->request->event->participationFee4()->first()->delete();
|
||||
}
|
||||
|
||||
$this->request->event->save();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\SetParticipationFees;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\RelationModels\EventParticipationFee;
|
||||
|
||||
class SetParticipationFeesRequest {
|
||||
public Event $event;
|
||||
public array $participationFeeFirst;
|
||||
public ?array $participationFeeSecond;
|
||||
public ?array $participationFeeThird;
|
||||
public ?array $participationFeeFourth;
|
||||
|
||||
|
||||
public function __construct(Event $event, array $participationFeeFirst) {
|
||||
$this->event = $event;
|
||||
$this->participationFeeFirst = $participationFeeFirst;
|
||||
$this->participationFeeSecond = null;
|
||||
$this->participationFeeThird = null;
|
||||
$this->participationFeeFourth = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\SetParticipationFees;
|
||||
|
||||
class SetParticipationFeesResponse {
|
||||
public bool $success;
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
}
|
||||
|
||||
}
|
||||
41
app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php
Normal file
41
app/Domains/Event/Actions/UpdateEvent/UpdateEventCommand.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\UpdateEvent;
|
||||
|
||||
class UpdateEventCommand {
|
||||
public UpdateEventRequest $request;
|
||||
public function __construct(UpdateEventRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : UpdateEventResponse {
|
||||
$response = new UpdateEventResponse();
|
||||
|
||||
$this->request->event->name = $this->request->eventName;
|
||||
$this->request->event->location = $this->request->eventLocation;
|
||||
$this->request->event->postal_code = $this->request->postalCode;
|
||||
$this->request->event->email = $this->request->email;
|
||||
$this->request->event->early_bird_end = $this->request->earlyBirdEnd;
|
||||
$this->request->event->registration_final_end = $this->request->registrationFinalEnd;
|
||||
$this->request->event->alcoholics_age = $this->request->alcoholicsAge;
|
||||
$this->request->event->support_per_person = $this->request->supportPerPerson;
|
||||
$this->request->event->support_flat = $this->request->flatSupport;
|
||||
$this->request->event->save();
|
||||
|
||||
$this->request->event->resetAllowedEatingHabits();
|
||||
$this->request->event->resetContributingLocalGroups();
|
||||
|
||||
foreach($this->request->eatingHabits as $eatingHabit) {
|
||||
$this->request->event->eatingHabits()->attach($eatingHabit);
|
||||
}
|
||||
|
||||
foreach($this->request->contributingLocalGroups as $contributingLocalGroup) {
|
||||
$this->request->event->localGroups()->attach($contributingLocalGroup);
|
||||
}
|
||||
|
||||
$this->request->event->save();
|
||||
$response->success = true;
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
41
app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
Normal file
41
app/Domains/Event/Actions/UpdateEvent/UpdateEventRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\UpdateEvent;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\ValueObjects\Amount;
|
||||
use DateTime;
|
||||
|
||||
class UpdateEventRequest {
|
||||
public Event $event;
|
||||
public string $eventName;
|
||||
public string $eventLocation;
|
||||
public string $postalCode;
|
||||
public string $email;
|
||||
public DateTime $earlyBirdEnd;
|
||||
public DateTime $registrationFinalEnd;
|
||||
public int $alcoholicsAge;
|
||||
public bool $sendWeeklyReports;
|
||||
public bool $registrationAllowed;
|
||||
public Amount $flatSupport;
|
||||
public Amount $supportPerPerson;
|
||||
public array $contributingLocalGroups;
|
||||
public array $eatingHabits;
|
||||
|
||||
public function __construct(Event $event, string $eventName, string $eventLocation, string $postalCode, string $email, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $alcoholicsAge, bool $sendWeeklyReports, bool $registrationAllowed, Amount $flatSupport, Amount $supportPerPerson, array $contributingLocalGroups, array $eatingHabits) {
|
||||
$this->event = $event;
|
||||
$this->eventName = $eventName;
|
||||
$this->eventLocation = $eventLocation;
|
||||
$this->postalCode = $postalCode;
|
||||
$this->email = $email;
|
||||
$this->earlyBirdEnd = $earlyBirdEnd;
|
||||
$this->registrationFinalEnd = $registrationFinalEnd;
|
||||
$this->alcoholicsAge = $alcoholicsAge;
|
||||
$this->sendWeeklyReports = $sendWeeklyReports;
|
||||
$this->registrationAllowed = $registrationAllowed;
|
||||
$this->flatSupport = $flatSupport;
|
||||
$this->supportPerPerson = $supportPerPerson;
|
||||
$this->contributingLocalGroups = $contributingLocalGroups;
|
||||
$this->eatingHabits = $eatingHabits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\UpdateEvent;
|
||||
|
||||
class UpdateEventResponse {
|
||||
public bool $success;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\UpdateManagers;
|
||||
|
||||
class UpdateManagersCommand {
|
||||
private UpdateManagersRequest $request;
|
||||
|
||||
public function __construct(UpdateManagersRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : UpdateManagersResponse {
|
||||
$response = new UpdateManagersResponse();
|
||||
$this->request->event->resetMangers();
|
||||
|
||||
foreach ($this->request->managers as $manager) {
|
||||
$this->request->event->eventManagers()->attach($manager);
|
||||
}
|
||||
|
||||
$this->request->event->save();
|
||||
$response->success = true;
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\UpdateManagers;
|
||||
|
||||
use App\Models\Event;
|
||||
|
||||
class UpdateManagersRequest {
|
||||
public Event $event;
|
||||
public array $managers;
|
||||
|
||||
public function __construct(Event $event, array $managers) {
|
||||
$this->managers = $managers;
|
||||
$this->event = $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Actions\UpdateManagers;
|
||||
|
||||
class UpdateManagersResponse {
|
||||
public bool $success;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
}
|
||||
}
|
||||
100
app/Domains/Event/Controllers/CreateController.php
Normal file
100
app/Domains/Event/Controllers/CreateController.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Controllers;
|
||||
|
||||
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitCommand;
|
||||
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
|
||||
use App\Domains\Event\Actions\CreateEvent\CreateEventCommand;
|
||||
use App\Domains\Event\Actions\CreateEvent\CreateEventRequest;
|
||||
use App\Domains\Event\Actions\SetCostUnit\SetCostUnitCommand;
|
||||
use App\Domains\Event\Actions\SetCostUnit\SetCostUnitRequest;
|
||||
use App\Enumerations\CostUnitType;
|
||||
use App\Enumerations\ParticipationFeeType;
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Resources\EventResource;
|
||||
use App\Scopes\CommonController;
|
||||
use App\ValueObjects\Amount;
|
||||
use DateTime;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CreateController extends CommonController {
|
||||
public function __invoke() {
|
||||
return new InertiaProvider('Event/Create', [
|
||||
'emailAddress' => auth()->user()->email,
|
||||
'eventAccount' => $this->tenant->account_name,
|
||||
'eventIban' => $this->tenant->account_iban,
|
||||
'eventPayPerDay' => $this->tenant->slug === 'lv' ? true : false,
|
||||
'participationFeeType' => $this->tenant->slug === 'lv' ?
|
||||
ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED :
|
||||
ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY,
|
||||
])->render();
|
||||
}
|
||||
|
||||
public function doCreate(Request $request) : JsonResponse {
|
||||
|
||||
$eventBegin = DateTime::createFromFormat('Y-m-d', $request->input('eventBegin'));
|
||||
$eventEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventEnd'));
|
||||
$eventEarlyBirdEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventEarlyBirdEnd'));
|
||||
$registrationFinalEnd = DateTime::createFromFormat('Y-m-d', $request->input('eventRegistrationFinalEnd'));
|
||||
$participationFeeType = ParticipationFeeType::where('slug', $request->input('eventParticipationFeeType'))->first();
|
||||
$payPerDay = $request->input('eventPayPerDay');
|
||||
$payDirect = $request->input('eventPayDirectly');
|
||||
|
||||
$billingDeadline = $eventEnd->modify('+1 month');
|
||||
|
||||
$createRequest = new CreateEventRequest(
|
||||
$request->input('eventName'),
|
||||
$request->input('eventLocation'),
|
||||
$request->input('eventPostalCode'),
|
||||
$request->input('eventEmail'),
|
||||
$eventBegin,
|
||||
$eventEnd,
|
||||
$eventEarlyBirdEnd,
|
||||
$registrationFinalEnd,
|
||||
$request->input('eventEarlyBirdEndAmountIncrease'),
|
||||
$participationFeeType,
|
||||
$request->input('eventAccount'),
|
||||
$request->input('eventIban'),
|
||||
$payPerDay,
|
||||
$payDirect
|
||||
);
|
||||
|
||||
$wasSuccessful = false;
|
||||
|
||||
$createCommand = new CreateEventCommand($createRequest);
|
||||
$result = $createCommand->execute();
|
||||
if ($result->success) {
|
||||
$createCostUnitRequest = new CreateCostUnitRequest(
|
||||
$result->event->name,
|
||||
CostUnitType::COST_UNIT_TYPE_EVENT,
|
||||
Amount::fromString('0,25'),
|
||||
true,
|
||||
$billingDeadline
|
||||
);
|
||||
|
||||
$createCostUnitCommand = new CreateCostUnitCommand($createCostUnitRequest);
|
||||
$costUnitResponse = $createCostUnitCommand->execute();
|
||||
|
||||
if ($costUnitResponse->success) {
|
||||
$costUnitUpdateRequest = new SetCostUnitRequest($result->event, $costUnitResponse->costUnit);
|
||||
$costUnitUpdateCommand = new SetCostUnitCommand($costUnitUpdateRequest);
|
||||
$costUnitSetResponse = $costUnitUpdateCommand->execute();
|
||||
$wasSuccessful = $costUnitSetResponse->success;
|
||||
}
|
||||
}
|
||||
|
||||
if ($wasSuccessful) {
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'event' => new EventResource($costUnitUpdateRequest->event)->toArray()
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Die Veranstaltung konnte nicht angelegt werden.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
118
app/Domains/Event/Controllers/DetailsController.php
Normal file
118
app/Domains/Event/Controllers/DetailsController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Controllers;
|
||||
|
||||
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesCommand;
|
||||
use App\Domains\Event\Actions\SetParticipationFees\SetParticipationFeesRequest;
|
||||
use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand;
|
||||
use App\Domains\Event\Actions\UpdateEvent\UpdateEventRequest;
|
||||
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersCommand;
|
||||
use App\Domains\Event\Actions\UpdateManagers\UpdateManagersRequest;
|
||||
use App\Enumerations\ParticipationFeeType;
|
||||
use App\Enumerations\ParticipationType;
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Resources\EventResource;
|
||||
use App\Scopes\CommonController;
|
||||
use App\ValueObjects\Amount;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DetailsController extends CommonController {
|
||||
public function __invoke(int $eventId) {
|
||||
$event = $this->events->getById($eventId);
|
||||
return new InertiaProvider('Event/Details', ['event' => $event])->render();
|
||||
}
|
||||
|
||||
public function summary(int $eventId) : JsonResponse {
|
||||
$event = $this->events->getById($eventId);
|
||||
return response()->json(['event' => new EventResource($event)->toArray()]);
|
||||
}
|
||||
|
||||
public function updateCommonSettings(int $eventId, Request $request) : JsonResponse {
|
||||
$event = $this->events->getById($eventId);
|
||||
|
||||
$earlyBirdEnd = \DateTime::createFromFormat('Y-m-d', $request->input('earlyBirdEnd'));
|
||||
$registrationFinalEnd = \DateTime::createFromFormat('Y-m-d', $request->input('registrationFinalEnd'));
|
||||
$flatSupport = Amount::fromString($request->input('flatSupport'));
|
||||
$supportPerPerson = Amount::fromString($request->input('supportPerson'));
|
||||
|
||||
$contributinLocalGroups = $request->input('contributingLocalGroups');
|
||||
$eatingHabits = $request->input('eatingHabits');
|
||||
|
||||
$eventUpdateRequest = new UpdateEventRequest(
|
||||
$event,
|
||||
$request->input('eventName'),
|
||||
$request->input('eventLocation'),
|
||||
$request->input('postalCode'),
|
||||
$request->input('email'),
|
||||
$earlyBirdEnd,
|
||||
$registrationFinalEnd,
|
||||
$request->input('alcoholicsAge'),
|
||||
$request->input('sendWeeklyReports'),
|
||||
$request->input('registrationAllowed'),
|
||||
$flatSupport,
|
||||
$supportPerPerson,
|
||||
$contributinLocalGroups,
|
||||
$eatingHabits
|
||||
);
|
||||
|
||||
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
|
||||
$response = $eventUpdateCommand->execute();
|
||||
|
||||
return response()->json(['status' => $response->success ? 'success' : 'error']);
|
||||
}
|
||||
|
||||
public function updateEventManagers(int $eventId, Request $request) : JsonResponse {
|
||||
$event = $this->events->getById($eventId);
|
||||
|
||||
$updateEventManagersRequest = new UpdateManagersRequest($event, $request->input('selectedManagers'));
|
||||
$updateEventManagersCommand = new UpdateManagersCommand($updateEventManagersRequest);
|
||||
$response = $updateEventManagersCommand->execute();
|
||||
return response()->json(['status' => $response->success ? 'success' : 'error']);
|
||||
}
|
||||
|
||||
public function updateParticipationFees(int $eventId, Request $request) : JsonResponse {
|
||||
$event = $this->events->getById($eventId);
|
||||
|
||||
$participationFeeFirst = [
|
||||
'type' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT,
|
||||
'name' => 'Teilnehmer',
|
||||
'description' => $request->input('pft_1_description'),
|
||||
'amount' => Amount::fromString($request->input('pft_1_amount'))
|
||||
];
|
||||
|
||||
$participationFeeRequest = new SetParticipationFeesRequest($event, $participationFeeFirst);
|
||||
|
||||
if ($request->input('pft_2_active')) {
|
||||
$participationFeeRequest->participationFeeSecond = [
|
||||
'type' => ParticipationType::PARTICIPATION_TYPE_TEAM,
|
||||
'name' => $event->participation_fee_type === ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED ? 'Kernteam' : 'Solidaritätsbeitrag',
|
||||
'description' => $request->input('pft_2_description'),
|
||||
'amount' => Amount::fromString($request->input('pft_2_amount'))
|
||||
];
|
||||
}
|
||||
|
||||
if ($request->input('pft_3_active')) {
|
||||
$participationFeeRequest->participationFeeThird = [
|
||||
'type' => ParticipationType::PARTICIPATION_TYPE_VOLUNTEER,
|
||||
'name' => $event->participation_fee_type === ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED ? 'Unterstützende' : 'Reduzierter Beitrag',
|
||||
'description' => $event->participation_fee_type !== ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY ? $request->input('pft_3_description') : 'Nach Verfügbarkeit',
|
||||
'amount' => Amount::fromString($request->input('pft_3_amount'))
|
||||
];
|
||||
}
|
||||
|
||||
if ($request->input('pft_4_active') && $event->participation_fee_type === ParticipationFeeType::PARTICIPATION_FEE_TYPE_FIXED) {
|
||||
$participationFeeRequest->participationFeeFourth = [
|
||||
'type' => ParticipationType::PARTICIPATION_TYPE_OTHER,
|
||||
'name' => 'Sonstige',
|
||||
'description' => $request->input('pft_4_description'),
|
||||
'amount' => Amount::fromString($request->input('pft_4_amount'))
|
||||
];
|
||||
}
|
||||
|
||||
$participationFeeCommand = new SetParticipationFeesCommand($participationFeeRequest);
|
||||
$response = $participationFeeCommand->excetute();
|
||||
|
||||
return response()->json(['status' => $response->success ? 'success' : 'error']);
|
||||
}
|
||||
}
|
||||
31
app/Domains/Event/Routes/api.php
Normal file
31
app/Domains/Event/Routes/api.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Event\Controllers\CreateController;
|
||||
use App\Domains\Event\Controllers\DetailsController;
|
||||
use App\Middleware\IdentifyTenant;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('api/v1')
|
||||
->group(function () {
|
||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
Route::prefix('event')->group(function () {
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/create', [CreateController::class, 'doCreate']);
|
||||
|
||||
Route::prefix('/details/{eventId}') ->group(function () {
|
||||
Route::get('/summary', [DetailsController::class, 'summary']);
|
||||
|
||||
|
||||
|
||||
|
||||
Route::post('/event-managers', [DetailsController::class, 'updateEventManagers']);
|
||||
Route::post('/participation-fees', [DetailsController::class, 'updateParticipationFees']);
|
||||
Route::post('/common-settings', [DetailsController::class, 'updateCommonSettings']);
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
15
app/Domains/Event/Routes/web.php
Normal file
15
app/Domains/Event/Routes/web.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Event\Controllers\CreateController;
|
||||
use App\Domains\Event\Controllers\DetailsController;
|
||||
use App\Middleware\IdentifyTenant;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
Route::prefix('event')->group(function () {
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/new', CreateController::class);
|
||||
Route::get('/details/{eventId}', DetailsController::class);
|
||||
});
|
||||
});
|
||||
});
|
||||
294
app/Domains/Event/Views/Create.vue
Normal file
294
app/Domains/Event/Views/Create.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<script setup>
|
||||
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
|
||||
import {reactive, watch, ref, computed} from 'vue'
|
||||
import { subWeeks, format, parseISO, isValid, addDays } from 'date-fns'
|
||||
import ErrorText from "../../../Views/Components/ErrorText.vue";
|
||||
import {useAjax} from "../../../../resources/js/components/ajaxHandler.js";
|
||||
import ParticipationFees from "./Partials/ParticipationFees.vue";
|
||||
|
||||
const { request } = useAjax();
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
'emailAddress': String,
|
||||
'eventAccount': String,
|
||||
'eventIban': String,
|
||||
"eventPayPerDay": Boolean,
|
||||
"participationFeeType": String,
|
||||
})
|
||||
|
||||
const errors = reactive({})
|
||||
|
||||
const formData = reactive({
|
||||
eventName: '',
|
||||
eventPostalCode: '',
|
||||
eventLocation: '',
|
||||
eventEmail: props.emailAddress ? props.emailAddress : '',
|
||||
eventBegin: '',
|
||||
eventEnd: '',
|
||||
eventEarlyBirdEnd: '',
|
||||
eventEarlyBirdEndAmountIncrease: 50,
|
||||
eventRegistrationFinalEnd: '',
|
||||
eventAccount: props.eventAccount ? props.eventAccount : '',
|
||||
eventIban: props.eventIban ? props.eventIban : '',
|
||||
eventPayDirectly: true,
|
||||
eventPayPerDay: props.eventPayPerDay ? props.eventPayPerDay : false,
|
||||
eventParticipationFeeType: props.participationFeeType ? props.participationFeeType : 'fixed',
|
||||
});
|
||||
|
||||
watch(
|
||||
() => formData.eventBegin,
|
||||
(newValue) => {
|
||||
if (!newValue) return
|
||||
|
||||
const beginDate = parseISO(newValue)
|
||||
|
||||
if (!isValid(beginDate)) return
|
||||
|
||||
const fourWeeksBefore = subWeeks(beginDate, 4)
|
||||
const twoWeeksBefore = subWeeks(beginDate, 2)
|
||||
const threeDaysAfter = addDays(beginDate, 2)
|
||||
|
||||
formData.eventEarlyBirdEnd = format(
|
||||
fourWeeksBefore,
|
||||
'yyyy-MM-dd'
|
||||
)
|
||||
|
||||
formData.eventRegistrationFinalEnd = format(
|
||||
twoWeeksBefore,
|
||||
'yyyy-MM-dd'
|
||||
)
|
||||
|
||||
formData.eventEnd = format(
|
||||
threeDaysAfter,
|
||||
'yyyy-MM-dd'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const formIsValid = computed(() => {
|
||||
errors.eventEmail = '';
|
||||
errors.eventName = '';
|
||||
errors.eventLocation = '';
|
||||
errors.eventPostalCode = '';
|
||||
errors.eventBegin = '';
|
||||
errors.eventEnd = '';
|
||||
errors.eventEarlyBirdEnd = '';
|
||||
errors.eventRegistrationFinalEnd = '';
|
||||
errors.eventAccount = '';
|
||||
errors.eventIban = '';
|
||||
|
||||
var returnValue = true;
|
||||
|
||||
if (!formData.eventName) {
|
||||
errors.eventName = 'Bitte gib den Veranstaltungsnamen ein'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventEmail) {
|
||||
errors.eventEmail = 'Bitte gib die E-Mail-Adresse der Veranstaltungsleitung für Rückfragen der Teilnehmenden ein'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventLocation) {
|
||||
errors.eventLocation = 'Bitte gib den Veranstaltungsort ein'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventPostalCode) {
|
||||
errors.eventPostalCode = 'Bitte gib die Postleitzahl des Veranstaltungsorts ein'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventBegin) {
|
||||
errors.eventBegin = 'Bitte gib das Anfangsdatum der Veranstaltung ein'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventEnd ||formData.eventEnd < formData.eventBegin ) {
|
||||
errors.eventEnd = 'Das Enddatum darf nicht vor dem Anfangsdatum liegen'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventEarlyBirdEnd ||formData.eventEarlyBirdEnd > formData.eventBegin ) {
|
||||
errors.eventEarlyBirdEnd = 'Das Enddatum der Early-Bird-Phase muss vor dem Veranstaltungsbeginn liegen'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventRegistrationFinalEnd ||formData.eventRegistrationFinalEnd > formData.eventBegin ) {
|
||||
errors.eventRegistrationFinalEnd = 'Der Anmeldeschluss darf nicht nach dem Veranstaltungsbeginn liegen'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventAccount) {
|
||||
errors.eventAccount = 'Bitte gib an, auf wen das Veranstaltungskonto für eingehende Beiträge läuft'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
if (!formData.eventIban) {
|
||||
errors.eventIban = 'Bitte gib die IBAN des Kontos für Teilnahmebeiträge ein'
|
||||
returnValue = false
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
|
||||
})
|
||||
|
||||
const showParticipationFees = ref(false)
|
||||
const newEvent = ref(null)
|
||||
|
||||
async function createEvent() {
|
||||
if (!formIsValid.value) return false
|
||||
|
||||
const data = await request("/api/v1/event/create", {
|
||||
method: "POST",
|
||||
body: {
|
||||
eventName: formData.eventName,
|
||||
eventPostalCode: formData.eventPostalCode,
|
||||
eventLocation: formData.eventLocation,
|
||||
eventEmail: formData.eventEmail,
|
||||
eventBegin: formData.eventBegin,
|
||||
eventEnd: formData.eventEnd,
|
||||
eventEarlyBirdEnd: formData.eventEarlyBirdEnd,
|
||||
eventEarlyBirdEndAmountIncrease: formData.eventEarlyBirdEndAmountIncrease,
|
||||
eventRegistrationFinalEnd: formData.eventRegistrationFinalEnd,
|
||||
eventAccount: formData.eventAccount,
|
||||
eventIban: formData.eventIban,
|
||||
eventPayDirectly: formData.eventPayDirectly,
|
||||
eventPayPerDay: formData.eventPayPerDay,
|
||||
eventParticipationFeeType: formData.eventParticipationFeeType,
|
||||
}
|
||||
});
|
||||
|
||||
if (data.status !== 'success') {
|
||||
toas.error(data.message);
|
||||
return false;
|
||||
} else {
|
||||
console.log(data.event);
|
||||
newEvent.value = data.event;
|
||||
showParticipationFees.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function finishCreation() {
|
||||
window.location.href = '/event/details/' + newEvent.value.id;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Neue Veranstaltung">
|
||||
<fieldset>
|
||||
<legend>
|
||||
<span style="font-weight: bolder;">Grundlegende Veranstaltungsdaten</span>
|
||||
</legend>
|
||||
<ParticipationFees v-if="showParticipationFees" :event="newEvent" @close="finishCreation" />
|
||||
|
||||
<table style="margin-top: 40px; width: 100%" v-else>
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Veranstaltungsname</th>
|
||||
<td class="height-50"><input type="text" v-model="formData.eventName" class="width-half-full" />
|
||||
<ErrorText :message="errors.eventName" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Veranstaltungsort</th>
|
||||
<td class="height-50"><input type="text" v-model="formData.eventLocation" class="width-half-full" />
|
||||
<ErrorText :message="errors.eventLocation" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Postleitzahl des Veranstaltungsorts</th>
|
||||
<td class="height-50"><input type="text" v-model="formData.eventPostalCode" class="width-half-full" />
|
||||
<ErrorText :message="errors.eventPostalCode" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">E-Mail-Adresse der Veranstaltungsleitung</th>
|
||||
<td class="height-50"><input type="email" v-model="formData.eventEmail" class="width-half-full" />
|
||||
<ErrorText :message="errors.eventEmail" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Beginn</th>
|
||||
<td class="height-50"><input type="date" v-model="formData.eventBegin" class="width-half-full" />
|
||||
<ErrorText :message="errors.eventBegin" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Ende</th>
|
||||
<td class="height-50"><input type="date" v-model="formData.eventEnd" class="width-half-full" />
|
||||
<ErrorText :message="errors.eventEnd" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Ende Early-Bird-Phase</th>
|
||||
<td class="height-50"><input type="date" v-model="formData.eventEarlyBirdEnd" class="width-half-full" />
|
||||
<ErrorText :message="errors.eventEarlyBirdEnd" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Finaler Anmeldeschluss</th>
|
||||
<td class="height-50"><input type="date" v-model="formData.eventRegistrationFinalEnd" class="width-half-full" />
|
||||
<ErrorText :message="errors.eventRegistrationFinalEnd" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Beitragsart</th>
|
||||
<td class="height-50">
|
||||
<select v-model="formData.eventParticipationFeeType" class="width-half-full">
|
||||
<option value="fixed">Festpreis</option>
|
||||
<option value="solidarity">Solidaritätsprinzip</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Preiserhöhung nach Early-Bird-Phase</th>
|
||||
<td class="height-50"><input type="number" v-model="formData.eventEarlyBirdEndAmountIncrease" class="width-tiny" />%</td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Veranstsaltungs-Konto</th>
|
||||
<td class="height-50"><input type="text" v-model="formData.eventAccount" class="width-full" />
|
||||
<ErrorText :message="errors.eventAccount" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<th class="width-medium pr-20 height-50">Veranstaltungs-IBAN</th>
|
||||
<td class="height-50"><input type="text" v-model="formData.eventIban" class="width-full" />
|
||||
<ErrorText :message="errors.eventIban" /></td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<td colspan="2" style="font-weight: bold;">
|
||||
<input type="checkbox" v-model="formData.eventPayDirectly">
|
||||
Teilnehmende zahlen direkt aufs Veranstaltungskonto
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="vertical-align: top;">
|
||||
<td colspan="2" style="font-weight: bold;">
|
||||
<input type="checkbox" v-model="formData.eventPayPerDay">
|
||||
Beitrag abhängig von Anwesenheitstagen
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" class="pt-20">
|
||||
<input type="button" value="Veranstaltung erstellen" @click="createEvent" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
</fieldset>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
60
app/Domains/Event/Views/Details.vue
Normal file
60
app/Domains/Event/Views/Details.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup>
|
||||
import {reactive, inject, onMounted} from 'vue';
|
||||
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
|
||||
import ListCostUnits from "../../CostUnit/Views/Partials/ListCostUnits.vue";
|
||||
import Overview from "./Partials/Overview.vue";
|
||||
|
||||
const props = defineProps({
|
||||
event: Object,
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: 'Übersicht',
|
||||
component: Overview,
|
||||
endpoint: "/api/v1/event/details/" + props.event.id + '/summary',
|
||||
},
|
||||
{
|
||||
title: 'Alle Teilnehmendenden',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/current-running-jobs",
|
||||
},
|
||||
{
|
||||
title: 'Teilis nach Stamm',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/closed-cost-units",
|
||||
},
|
||||
{
|
||||
title: 'Teilis nach Teili-Gruppe',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/archived-cost-units",
|
||||
},
|
||||
{
|
||||
title: 'Abgemeldete Teilis',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/archived-cost-units",
|
||||
},
|
||||
{
|
||||
title: 'Zusätze',
|
||||
component: ListCostUnits,
|
||||
endpoint: "/api/v1/cost-unit/open/archived-cost-units",
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (undefined !== props.message) {
|
||||
toast.success(props.message)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :title="'Veranstaltungsdetails ' + props.event.name">
|
||||
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
|
||||
<tabbed-page :tabs="tabs" />
|
||||
|
||||
</shadowed-box>
|
||||
</AppLayout>
|
||||
</template>
|
||||
258
app/Domains/Event/Views/Partials/CommonSettings.vue
Normal file
258
app/Domains/Event/Views/Partials/CommonSettings.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import ErrorText from "../../../../Views/Components/ErrorText.vue";
|
||||
import AmountInput from "../../../../Views/Components/AmountInput.vue";
|
||||
import {request} from "../../../../../resources/js/components/HttpClient.js";
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
event: Object,
|
||||
})
|
||||
|
||||
|
||||
const dynmicProps = reactive({
|
||||
localGroups: [],
|
||||
eatingHabits:[]
|
||||
});
|
||||
|
||||
const contributingLocalGroups = ref([])
|
||||
const eatingHabits = ref([]);
|
||||
|
||||
const errors = reactive({})
|
||||
|
||||
const formData = reactive({
|
||||
contributingLocalGroups: contributingLocalGroups.value,
|
||||
eventName: props.event.name,
|
||||
eventLocation: props.event.location,
|
||||
postalCode: props.event.postalCode,
|
||||
email: props.event.email,
|
||||
earlyBirdEnd: props.event.earlyBirdEnd.internal,
|
||||
registrationFinalEnd: props.event.registrationFinalEnd.internal,
|
||||
alcoholicsAge: props.event.alcoholicsAge,
|
||||
eatingHabits: eatingHabits.value,
|
||||
sendWeeklyReports: props.event.sendWeeklyReports,
|
||||
registrationAllowed: props.event.registrationAllowed,
|
||||
flatSupport: props.event.flatSupportEdit,
|
||||
supportPerson: props.event.supportPersonEdit,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/core/retrieve-event-setting-data');
|
||||
const data = await response.json();
|
||||
Object.assign(dynmicProps, data);
|
||||
|
||||
contributingLocalGroups.value = props.event.contributingLocalGroups?.map(t => t.id) ?? []
|
||||
eatingHabits.value = props.event.eatingHabits?.map(t => t.id) ?? []
|
||||
});
|
||||
|
||||
async function save() {
|
||||
const response = await request('/api/v1/event/details/' + props.event.id + '/common-settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: {
|
||||
eventName: formData.eventName,
|
||||
eventLocation: formData.eventLocation,
|
||||
postalCode: formData.postalCode,
|
||||
email: formData.email,
|
||||
earlyBirdEnd: formData.earlyBirdEnd,
|
||||
registrationFinalEnd: formData.registrationFinalEnd,
|
||||
alcoholicsAge: formData.alcoholicsAge,
|
||||
sendWeeklyReports: formData.sendWeeklyReports,
|
||||
registrationAllowed: formData.registrationAllowed,
|
||||
flatSupport: formData.flatSupport,
|
||||
supportPerson: formData.supportPerson,
|
||||
contributingLocalGroups: contributingLocalGroups.value,
|
||||
eatingHabits: eatingHabits.value,
|
||||
}
|
||||
})
|
||||
|
||||
if (response.status === 'success') {
|
||||
toast.success('Einstellungen wurden erfolgreich gespeichert.')
|
||||
emit('close')
|
||||
} else {
|
||||
toast.error('Beim Speichern ist ein Fehler aufgetreten.')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>Einstellungen</h2>
|
||||
<div class="container">
|
||||
<div class="row top">
|
||||
<div class="left">
|
||||
<table class="event-settings-table" style="width: 80%;">
|
||||
<tr>
|
||||
<th>Veranstaltungsname</th>
|
||||
<td>
|
||||
<input type="text" v-model="formData.eventName" class="width-full" /><br />
|
||||
<ErrorText :message="errors.eventName" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Veranstaltungsort</th>
|
||||
<td>
|
||||
<input type="text" v-model="formData.eventLocation" class="width-full" /><br />
|
||||
<ErrorText :message="errors.eventLocation" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Postleitzahl des Veranstaltungsorts</th>
|
||||
<td>
|
||||
<input type="text" v-model="formData.postalCode" class="width-full" /><br />
|
||||
<ErrorText :message="errors.eventPostalCode" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>E-Mail-Adresse der Veranstaltungsleitung</th>
|
||||
<td>
|
||||
<input type="text" v-model="formData.email" class="width-full" /><br />
|
||||
<ErrorText :message="errors.eventEmail" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Ende der EarlyBird-Phase</th>
|
||||
<td>
|
||||
<input type="date" v-model="formData.earlyBirdEnd" class="width-full" /><br />
|
||||
<ErrorText :message="errors.earlyBirdEnd" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Finaler Anmeldeschluss</th>
|
||||
<td>
|
||||
<input type="date" v-model="formData.registrationFinalEnd" class="width-full" /><br />
|
||||
<ErrorText :message="errors.registrationFinalEnd" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Fördermittel</th>
|
||||
<td>
|
||||
<amountInput v-model="formData.supportPerson" clasS="width-small" /> Euro p.P. / Tag
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Zuschüsse</th>
|
||||
<td>
|
||||
<amountInput v-model="formData.flatSupport" clasS="width-small" /> Euro pauschal
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Mindestalter für Alkoholkonsum</th>
|
||||
<td>
|
||||
<input type="number" v-model="formData.alcoholicsAge" class="width-tiny" /><br />
|
||||
<ErrorText :message="errors.alcoholicsAge" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" style="height: 25px !important;">
|
||||
<input type="checkbox" v-model="formData.sendWeeklyReports" id="sendWeeklyReports" />
|
||||
<label for="sendWeeklyReports">Wöchentliche Zusammenfassung per E-Mail an Stämme schicken</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="checkbox" v-model="formData.registrationAllowed" id="registrationAllowed" />
|
||||
<label for="registrationAllowed">Veranstaltung ist für Anmeldungen geöffnet</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="right">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Teilnehmende Stämme</th>
|
||||
</tr>
|
||||
<tr v-for="localGroup in dynmicProps.localGroups">
|
||||
<td>
|
||||
<input type="checkbox" :id="'localgroup_' + localGroup.id" :value="localGroup.id" v-model="contributingLocalGroups" />
|
||||
<label style="padding-left: 5px;" :for="'localgroup_' + localGroup.id">{{localGroup.name}}</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th style="padding-top: 40px !important;">Angebotene Ernährung</th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="eatingHabit in dynmicProps.eatingHabits">
|
||||
<td>
|
||||
<input type="checkbox" :id="'eatinghabit' + eatingHabit.id" :value="eatingHabit.id" v-model="eatingHabits" />
|
||||
<label style="padding-left: 5px;" :for="'eatinghabit' + eatingHabit.id">{{eatingHabit.name}}</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row bott">
|
||||
<input type="button" value="Speichern" @click="save" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px; /* Abstand zwischen den Zeilen */
|
||||
width: 95%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 10px; /* Abstand zwischen den Spalten */
|
||||
}
|
||||
|
||||
.row.top .left {
|
||||
flex: 0 0 70%; /* feste Breite von 80% */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.row.top .right {
|
||||
flex: 0 0 30%; /* feste Breite von 20% */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.row.bottom {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.event-settings-table {
|
||||
|
||||
}
|
||||
|
||||
.event-settings-table tr {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.event-settings-table td {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.event-settings-table th {
|
||||
vertical-align: top;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
</style>
|
||||
63
app/Domains/Event/Views/Partials/EventManagement.vue
Normal file
63
app/Domains/Event/Views/Partials/EventManagement.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup>
|
||||
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
import {request} from "../../../../../resources/js/components/HttpClient.js";
|
||||
const selectedManagers = ref([])
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const props = defineProps({
|
||||
event: Object
|
||||
})
|
||||
|
||||
|
||||
const commonProps = reactive({
|
||||
activeUsers: [],
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/core/retrieve-global-data');
|
||||
const data = await response.json();
|
||||
Object.assign(commonProps, data);
|
||||
|
||||
selectedManagers.value = props.event.managers?.map(t => t.id) ?? []
|
||||
});
|
||||
|
||||
|
||||
async function updateManagers() {
|
||||
|
||||
const response = await request('/api/v1/event/details/' + props.event.id + '/event-managers', {
|
||||
method: "POST",
|
||||
body: {
|
||||
selectedManagers: selectedManagers.value,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 'success') {
|
||||
toast.success('Einstellungen wurden erfolgreich gespeichert.')
|
||||
emit('close')
|
||||
} else {
|
||||
toast.error('Beim Speichern ist ein Fehler aufgetreten.')
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3>Aktionsleitung:</h3>
|
||||
<p v-for="user in commonProps.activeUsers">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="'user_' + user.id"
|
||||
:value="user.id"
|
||||
v-model="selectedManagers"
|
||||
/>
|
||||
<label :for="'user_' + user.id">{{user.fullname}}</label>
|
||||
</p>
|
||||
|
||||
<input type="button" value="Speichern" @click="updateManagers" />
|
||||
</template>
|
||||
|
||||
113
app/Domains/Event/Views/Partials/Overview.vue
Normal file
113
app/Domains/Event/Views/Partials/Overview.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import ParticipationFees from "./ParticipationFees.vue";
|
||||
import ParticipationSummary from "./ParticipationSummary.vue";
|
||||
import CommonSettings from "./CommonSettings.vue";
|
||||
import EventManagement from "./EventManagement.vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
})
|
||||
|
||||
const dynamicProps = reactive({
|
||||
event : null,
|
||||
});
|
||||
|
||||
const displayData = ref('main');
|
||||
|
||||
async function showMain() {
|
||||
const response = await fetch("/api/v1/event/details/" + props.data.event.id + '/summary');
|
||||
const data = await response.json();
|
||||
Object.assign(dynamicProps, data);
|
||||
displayData.value = 'main';
|
||||
}
|
||||
|
||||
async function showCommonSettings() {
|
||||
displayData.value = 'commonSettings';
|
||||
}
|
||||
|
||||
async function showParticipationFees() {
|
||||
displayData.value = 'participationFees';
|
||||
}
|
||||
|
||||
async function showEventManagement() {
|
||||
displayData.value = 'eventManagement';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch("/api/v1/event/details/" + props.data.event.id + '/summary');
|
||||
const data = await response.json();
|
||||
Object.assign(dynamicProps, data);
|
||||
|
||||
console.log(dynamicProps.event)
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ParticipationFees v-if="displayData === 'participationFees'" :event="dynamicProps.event" @close="showMain" />
|
||||
<CommonSettings v-else-if="displayData === 'commonSettings'" :event="dynamicProps.event" @close="showMain" />
|
||||
<EventManagement v-else-if="displayData === 'eventManagement'" :event="dynamicProps.event" @close="showMain" />
|
||||
|
||||
|
||||
|
||||
<div class="event-flexbox" v-else>
|
||||
<div class="event-flexbox-row top">
|
||||
<div class="left"><ParticipationSummary :event="dynamicProps.event" /></div>
|
||||
<div class="right">
|
||||
<input type="button" value="Erste-Hilfe-Liste (PDF)" /><br/>
|
||||
<input type="button" value="Teili-Liste (CSV)" /><br/>
|
||||
<input type="button" value="KSV-Daten (CSV)" /><br/>
|
||||
<input type="button" value="Küchenübersicht (PDF)" /><br/>
|
||||
<input type="button" value="Beitragsliste (PDF)" /><br/>
|
||||
<input type="button" value="Getränkeliste (PDF)" /><br/>
|
||||
<input type="button" value="Foto-Erlaubnis (PDF)" /><br/>
|
||||
<input type="button" class="fix-button" value="Zahlungserinnerung senden" /><br/>
|
||||
<input type="button" class="deny-button" value="Letzte Mahnung senden" /><br/>
|
||||
<input type="button" value="Rundmail senden" /><br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-flexbox-row bottom">
|
||||
<label style="font-size: 9pt;" class="link" @click="showCommonSettings">Allgemeine Einstellungen</label>
|
||||
<label style="font-size: 9pt;" class="link" @click="showEventManagement">Veranstaltungsleitung</label>
|
||||
<label style="font-size: 9pt;" class="link" @click="showParticipationFees">Teilnahmegebühren</label>
|
||||
<a style="font-size: 9pt;" class="link" :href="'/cost-unit/' + props.data.event.costUnit.id">Ausgabenübersicht</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.event-flexbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 95%;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
|
||||
.event-flexbox-row {
|
||||
display: flex;
|
||||
gap: 10px; /* Abstand zwischen den Spalten */
|
||||
}
|
||||
|
||||
.event-flexbox-row.top .left {
|
||||
flex: 0 0 calc(100% - 300px);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.event-flexbox-row.top .right {
|
||||
flex: 0 0 250px; /* feste Breite von 20% */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.event-flexbox-row.bottom {
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.event-flexbox-row.top .right input[type="button"] {
|
||||
width: 100% !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
231
app/Domains/Event/Views/Partials/ParticipationFees.vue
Normal file
231
app/Domains/Event/Views/Partials/ParticipationFees.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<script setup>
|
||||
import AppLayout from "../../../../../resources/js/layouts/AppLayout.vue";
|
||||
import ShadowedBox from "../../../../Views/Components/ShadowedBox.vue";
|
||||
import {reactive, watch} from "vue";
|
||||
import AmountInput from "../../../../Views/Components/AmountInput.vue";
|
||||
import ErrorText from "../../../../Views/Components/ErrorText.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
import {request} from "../../../../../resources/js/components/HttpClient.js";
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const props = defineProps({
|
||||
event: Object,
|
||||
})
|
||||
|
||||
const errors = reactive({})
|
||||
const formData = reactive({
|
||||
"pft_1_active": true,
|
||||
"pft_1_amount": props.event.participationFee_1.amount,
|
||||
"pft_1_description": props.event.participationFee_1.description,
|
||||
|
||||
"pft_2_active": props.event.participationFee_2.active,
|
||||
"pft_2_amount": props.event.participationFee_2.amount,
|
||||
"pft_2_description": props.event.participationFee_2.description,
|
||||
|
||||
"pft_3_active": props.event.participationFee_3.active,
|
||||
"pft_3_amount": props.event.participationFee_3.amount,
|
||||
"pft_3_description": props.event.participationFee_3.description,
|
||||
|
||||
"pft_4_active": props.event.participationFee_4.active,
|
||||
"pft_4_amount": props.event.participationFee_4.amount,
|
||||
"pft_4_description": props.event.participationFee_4.description,
|
||||
|
||||
'maxAmount': props.event.maxAmount,
|
||||
|
||||
})
|
||||
|
||||
function validateInput() {
|
||||
var noErrors = true;
|
||||
|
||||
if (formData.pft_1_description === '' && !props.event.solidarityPayment) {
|
||||
errors.pft_1_description = 'Eine Beschreibung für diese Gruppe ist erforderlich';
|
||||
noErrors = false;
|
||||
}
|
||||
|
||||
if (formData.pft_2_description === '' && formData.pft_2_active && !props.event.solidarityPayment) {
|
||||
errors.pft_2_description = 'Eine Beschreibung für diese Gruppe ist erforderlich';
|
||||
noErrors = false;
|
||||
}
|
||||
|
||||
if (formData.pft_3_description === '' && formData.pft_3_active && !props.event.solidarityPayment) {
|
||||
errors.pft_3_description = 'Eine Beschreibung für diese Gruppe ist erforderlich';
|
||||
noErrors = false;
|
||||
}
|
||||
|
||||
if (formData.pft_4_description === '' && formData.pft_4_active) {
|
||||
errors.pft_4_description = 'Eine Beschreibung für diese Gruppe ist erforderlich';
|
||||
noErrors = false;
|
||||
}
|
||||
|
||||
return noErrors;
|
||||
|
||||
}
|
||||
|
||||
async function saveParticipationFees() {
|
||||
if (!validateInput()) {
|
||||
toast.error('Bitte prüfe alle Eingaben auf Fehler')
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await request('/api/v1/event/details/' + props.event.id + '/participation-fees', {
|
||||
method: "POST",
|
||||
body: {
|
||||
event_id: props.event.id,
|
||||
pft_1_active: formData.pft_1_active,
|
||||
pft_1_amount: formData.pft_1_amount,
|
||||
pft_1_description: formData.pft_1_description,
|
||||
|
||||
pft_2_active: formData.pft_2_active,
|
||||
pft_2_amount: formData.pft_2_amount,
|
||||
pft_2_description: formData.pft_2_description,
|
||||
|
||||
pft_3_active: formData.pft_3_active,
|
||||
pft_3_amount: formData.pft_3_amount,
|
||||
pft_3_description: formData.pft_3_description,
|
||||
|
||||
pft_4_active: formData.pft_4_active,
|
||||
pft_4_amount: formData.pft_4_amount,
|
||||
pft_4_description: formData.pft_4_description,
|
||||
|
||||
maxAmount: formData.maxAmount,
|
||||
}
|
||||
})
|
||||
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function recalculateMaxAmount(newValue) {
|
||||
if (formData.maxAmount === 0) return;
|
||||
|
||||
var newAmount = parseFloat(newValue.replace(',', '.'));
|
||||
if (props.event.payPerDay) {
|
||||
newAmount = newAmount * props.event.duration;
|
||||
}
|
||||
|
||||
var currentMaxAmount = formData.maxAmount.replace(',', '.');
|
||||
|
||||
if (newAmount > currentMaxAmount) {
|
||||
formData.maxAmount = newAmount.toFixed(2).replace('.', ',');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>Aktiv</td>
|
||||
<td>Preisgruppe</td>
|
||||
<td>Betrag</td>
|
||||
<td>Beschreibung</td>
|
||||
</tr>
|
||||
<tr style="height: 65px; vertical-align: top">
|
||||
<td>
|
||||
<input type="checkbox" v-model="formData.participationFeeType_1" checked disabled/>
|
||||
</td>
|
||||
<td v-if="props.event.solidarityPayment">
|
||||
Regulärer Beitrag
|
||||
</td>
|
||||
<td v-else>
|
||||
Teilnehmende
|
||||
</td>
|
||||
<td>
|
||||
<AmountInput v-model="formData.pft_1_amount" class="width-small" @blur="recalculateMaxAmount(formData.pft_1_amount)" />
|
||||
<label v-if="props.event.payPerDay"> Euro / Tag</label>
|
||||
<label v-else> Euro Gesamt</label>
|
||||
</td>
|
||||
<td>
|
||||
<input v-if="!props.event.solidarityPayment" type="text" v-model="formData.pft_1_description" style="width: 300px;" />
|
||||
<label v-else></label>
|
||||
<ErrorText :message="errors.pft_1_description" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="height: 65px; vertical-align: top;">
|
||||
<td>
|
||||
<input id="use_pft_2" type="checkbox" v-model="formData.pft_2_active" :checked="formData.pft_2_active" />
|
||||
</td>
|
||||
<td v-if="props.event.solidarityPayment">
|
||||
<label for="use_pft_2" style="cursor: default">
|
||||
Solidaritätsbeitrag
|
||||
</label>
|
||||
</td>
|
||||
<td v-else>
|
||||
<label for="use_pft_2" style="cursor: default">
|
||||
Kernteam
|
||||
</label>
|
||||
</td>
|
||||
<td v-if="formData.pft_2_active">
|
||||
<AmountInput v-model="formData.pft_2_amount" class="width-small" @blur="recalculateMaxAmount(formData.pft_2_amount)" />
|
||||
<label v-if="props.event.payPerDay"> Euro / Tag</label>
|
||||
<label v-else> Euro Gesamt</label>
|
||||
</td>
|
||||
<td v-if="formData.pft_2_active">
|
||||
<input v-if="!props.event.solidarityPayment" type="text" v-model="formData.pft_2_description" style="width: 300px;" />
|
||||
<label v-else></label>
|
||||
<ErrorText :message="errors.pft_2_description" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="height: 65px; vertical-align: top;">
|
||||
<td>
|
||||
<input id="use_pft_3" type="checkbox" v-model="formData.pft_3_active" :checked="formData.pft_3_active" />
|
||||
</td>
|
||||
<td v-if="props.event.solidarityPayment">
|
||||
<label for="use_pft_3" style="cursor: default">
|
||||
Reduzierter Beitrag
|
||||
</label>
|
||||
</td>
|
||||
<td v-else>
|
||||
<label for="use_pft_3" style="cursor: default">
|
||||
Unterstützende
|
||||
</label>
|
||||
</td>
|
||||
<td v-if="formData.pft_3_active">
|
||||
<AmountInput v-model="formData.pft_3_amount" class="width-small" @blur="recalculateMaxAmount(formData.pft_3_amount)" />
|
||||
<label v-if="props.event.payPerDay"> Euro / Tag</label>
|
||||
<label v-else> Euro Gesamt</label>
|
||||
</td>
|
||||
<td v-if="formData.pft_3_active">
|
||||
<input v-if="!props.event.solidarityPayment" type="text" v-model="formData.pft_3_description" style="width: 300px;" />
|
||||
<label v-else>Nach Verfügbarkeit</label>
|
||||
<ErrorText :message="errors.pft_3_description" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="height: 65px; vertical-align: top;" v-if="!props.event.solidarityPayment">
|
||||
<td>
|
||||
<input id="use_pft_4" type="checkbox" v-model="formData.pft_4_active" :checked="formData.pft_4_active" />
|
||||
</td>
|
||||
<td>
|
||||
<label for="use_pft_4" style="cursor: default">
|
||||
Sonstige
|
||||
</label>
|
||||
</td>
|
||||
<td v-if="formData.pft_4_active">
|
||||
<AmountInput v-model="formData.pft_4_amount" class="width-small" @blur="recalculateMaxAmount(formData.pft_4_amount)" />
|
||||
<label v-if="props.event.payPerDay"> Euro / Tag</label>
|
||||
<label v-else> Euro Gesamt</label>
|
||||
</td>
|
||||
<td v-if="formData.pft_4_active">
|
||||
<input type="text" v-model="formData.pft_4_description" style="width: 300px;" />
|
||||
<ErrorText :message="errors.pft_4_description" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
Maximaler Beitrag für Veranstaltung:
|
||||
</td>
|
||||
<td colspan="2">
|
||||
<AmountInput v-model="formData.maxAmount" class="width-small" /> Euro Gesamt
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<input type="button" value="Speichern" @click="saveParticipationFees" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
125
app/Domains/Event/Views/Partials/ParticipationSummary.vue
Normal file
125
app/Domains/Event/Views/Partials/ParticipationSummary.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
event: Object
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>Übersicht</h2>
|
||||
<div class="participant-flexbox">
|
||||
<div class="participant-flexbox-row top">
|
||||
<div class="left">
|
||||
<h3>Teilnehmende</h3>
|
||||
<table class="participant-income-table" style="margin-bottom: 40px;">
|
||||
<tr>
|
||||
<th>Teili</th>
|
||||
<td>7 Personen:</td>
|
||||
<td>35,00 Euro</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Teili</th>
|
||||
<td>7 Personen:</td>
|
||||
<td>35,00 Euro</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Teili</th>
|
||||
<td>7 Personen:</td>
|
||||
<td>35,00 Euro</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th colspan="2">Sonstiges</th>
|
||||
<td>{{ props.event.flatSupport }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th style="padding-bottom: 20px" colspan="2">Förderung</th>
|
||||
<td style="padding-bottom: 20px">
|
||||
{{ props.event.supportPersonCalced }}<br />
|
||||
<label style="font-size: 9pt;">({{ props.event.supportPerson }} / Tag p.P.)</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<th colspan="2" style="border-width: 1px; border-bottom-style: solid">Gesamt</th>
|
||||
<td style="font-weight: bold; border-width: 1px; border-bottom-style: solid">{{ props.event.totalIncome }}</td>
|
||||
</tr>
|
||||
|
||||
<tr style="color:#4caf50;" v-if="props.event.totalBalance.value >= 0">
|
||||
<th style="padding-top: 20px; font-size: 12pt !important;" colspan="2">Bilanz</th>
|
||||
<td style="font-weight: bold; padding-top: 20px; font-size: 12pt !important;">{{ props.event.totalBalance.text }}</td>
|
||||
</tr>
|
||||
|
||||
<tr style="color:#f44336;" v-else>
|
||||
<th style="padding-top: 20px; font-size: 12pt !important;" colspan="2">Bilanz</th>
|
||||
<td style="font-weight: bold; padding-top: 20px; font-size: 12pt !important;">{{ props.event.totalBalance.text }}</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<strong>Anmelde-URL: {{props.event.url}}</strong>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<h3>Ausgaben</h3>
|
||||
<table class="event-payment-table">
|
||||
<tr v-for="amount in props.event.costUnit.amounts">
|
||||
<th>{{amount.name}}</th>
|
||||
<td>{{amount.string}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="color:#f44336; border-width: 1px; border-bottom-style: solid; padding-top: 20px">Gesamt</th>
|
||||
<td style="color:#f44336; border-width: 1px; border-bottom-style: solid; padding-top: 20px; font-weight: bold">{{props.event.costUnit.overAllAmount.text}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.participant-flexbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 95%;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
|
||||
.participant-flexbox-row {
|
||||
display: flex;
|
||||
gap: 10px; /* Abstand zwischen den Spalten */
|
||||
}
|
||||
|
||||
.participant-flexbox-row.top .left {
|
||||
flex: 0 0 50%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.participant-flexbox.top .right {
|
||||
flex: 0 0 50%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.participant-income-table,
|
||||
.event-payment-table {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.participant-income-table th {
|
||||
width: 20px;
|
||||
font-size: 11pt !important;
|
||||
}
|
||||
|
||||
.participant-income-table tr td:first-child {
|
||||
width: 25px !important;
|
||||
font-size: 11pt;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\ChangeStatus;
|
||||
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
|
||||
class ChangeStatusCommand {
|
||||
private ChangeStatusRequest $request;
|
||||
|
||||
public function __construct(ChangeStatusRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : ChangeStatusResponse {
|
||||
$response = new ChangeStatusResponse();
|
||||
|
||||
switch ($this->request->status) {
|
||||
case InvoiceStatus::INVOICE_STATUS_APPROVED:
|
||||
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_APPROVED;
|
||||
$this->request->invoice->approved_by = auth()->user()->id;
|
||||
$this->request->invoice->approved_at = now();
|
||||
break;
|
||||
|
||||
case InvoiceStatus::INVOICE_STATUS_DENIED:
|
||||
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_DENIED;
|
||||
$this->request->invoice->denied_by = auth()->user()->id;
|
||||
$this->request->invoice->denied_at = now();
|
||||
$this->request->invoice->denied_reason = $this->request->comment;
|
||||
break;
|
||||
case InvoiceStatus::INVOICE_STATUS_NEW:
|
||||
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_NEW;
|
||||
$this->request->invoice->approved_by = null;
|
||||
$this->request->invoice->approved_at = null;
|
||||
$this->request->invoice->denied_by = null;
|
||||
$this->request->invoice->denied_at = null;
|
||||
$this->request->invoice->comment = $this->request->invoice->denied_reason;
|
||||
$this->request->invoice->denied_reason = null;
|
||||
break;
|
||||
case InvoiceStatus::INVOICE_STATUS_DELETED:
|
||||
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_DELETED;
|
||||
break;
|
||||
case InvoiceStatus::INVOICE_STATUS_EXPORTED:
|
||||
$this->request->invoice->status = InvoiceStatus::INVOICE_STATUS_EXPORTED;
|
||||
$this->request->invoice->upload_required = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->request->invoice->save()) {
|
||||
$response->success = true;
|
||||
}
|
||||
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\ChangeStatus;
|
||||
|
||||
use App\Models\Invoice;
|
||||
|
||||
class ChangeStatusRequest {
|
||||
public Invoice $invoice;
|
||||
public string $status;
|
||||
public ?string $comment;
|
||||
|
||||
public function __construct(Invoice $invoice, string $status, ?string $comment = null) {
|
||||
$this->invoice = $invoice;
|
||||
$this->status = $status;
|
||||
$this->comment = $comment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\ChangeStatus;
|
||||
|
||||
class ChangeStatusResponse {
|
||||
public bool $success;
|
||||
public string $invoiceReceipt;
|
||||
|
||||
function __construct() {
|
||||
$this->success = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\CreateInvoice;
|
||||
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
use App\Models\Invoice;
|
||||
|
||||
class CreateInvoiceCommand {
|
||||
private CreateInvoiceRequest $request;
|
||||
|
||||
public function __construct(CreateInvoiceRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : CreateInvoiceResponse {
|
||||
$response = new CreateInvoiceResponse();
|
||||
|
||||
if ($this->request->accountIban === 'undefined') {
|
||||
$this->request->accountIban = null;
|
||||
}
|
||||
|
||||
$invoice = Invoice::create([
|
||||
'tenant' => app('tenant')->slug,
|
||||
'cost_unit_id' => $this->request->costUnit->id,
|
||||
'invoice_number' => $this->generateInvoiceNumber(),
|
||||
'status' => InvoiceStatus::INVOICE_STATUS_NEW,
|
||||
'type' => $this->request->invoiceType,
|
||||
'type_other' => $this->request->invoiceTypeExtended,
|
||||
'donation' => $this->request->isDonation,
|
||||
'user_id' => $this->request->userId,
|
||||
'contact_name' => $this->request->contactName,
|
||||
'contact_email' => $this->request->contactEmail,
|
||||
'contact_phone' => $this->request->contactPhone,
|
||||
'contact_bank_owner' => $this->request->accountOwner,
|
||||
'contact_bank_iban' => $this->request->accountIban,
|
||||
'amount' => $this->request->totalAmount,
|
||||
'distance' => $this->request->distance,
|
||||
'travel_direction' => $this->request->travelRoute,
|
||||
'passengers' => $this->request->passengers,
|
||||
'transportation' => $this->request->transportations,
|
||||
'document_filename' => $this->request->receiptFile !== null ? $this->request->receiptFile->fullPath : null,
|
||||
]);
|
||||
|
||||
if ($invoice !== null) {
|
||||
$response->success = true;
|
||||
$response->invoice = $invoice;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
private function generateInvoiceNumber() : string {
|
||||
$lastInvoiceNumber = Invoice::query()
|
||||
->where('tenant', app('tenant')->slug)
|
||||
->whereYear('created_at', date('Y'))
|
||||
->count();
|
||||
|
||||
$invoiceNumber = $lastInvoiceNumber + 1;
|
||||
$invoiceNumber = str_pad($invoiceNumber, 4, '0', STR_PAD_LEFT);
|
||||
return sprintf('%1$s-%2$s', date('Y'), $invoiceNumber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\CreateInvoice;
|
||||
|
||||
use App\Models\CostUnit;
|
||||
use App\ValueObjects\InvoiceFile;
|
||||
|
||||
class CreateInvoiceRequest {
|
||||
public CostUnit $costUnit;
|
||||
public string $contactName;
|
||||
public ?string $contactEmail;
|
||||
public ?string $contactPhone;
|
||||
public ?string $accountOwner;
|
||||
public ?string $accountIban;
|
||||
public string $invoiceType;
|
||||
public ?string $invoiceTypeExtended;
|
||||
public ?string $travelRoute;
|
||||
public ?int $distance;
|
||||
public ?int $passengers;
|
||||
public ?int $transportations;
|
||||
public ?InvoiceFile $receiptFile;
|
||||
public float $totalAmount;
|
||||
public bool $isDonation;
|
||||
public ?int $userId;
|
||||
|
||||
|
||||
public function __construct(
|
||||
CostUnit $costUnit,
|
||||
string $contactName,
|
||||
string $invoiceType,
|
||||
float $totalAmount,
|
||||
?InvoiceFile $receiptFile,
|
||||
bool $isDonation,
|
||||
?int $userId = null,
|
||||
?string $contactEmail = null,
|
||||
?string $contactPhone = null,
|
||||
?string $accountOwner = null,
|
||||
?string $accountIban = null,
|
||||
?string $invoiceTypeExtended = null,
|
||||
?string $travelRoute = null,
|
||||
?int $distance = null,
|
||||
?int $passengers = null,
|
||||
?int $transportations,
|
||||
|
||||
) {
|
||||
$this->costUnit = $costUnit;
|
||||
$this->contactName = $contactName;
|
||||
$this->invoiceType = $invoiceType;
|
||||
$this->invoiceTypeExtended = $invoiceTypeExtended;
|
||||
$this->travelRoute = $travelRoute;
|
||||
$this->distance = $distance;
|
||||
$this->passengers = $passengers;
|
||||
$this->transportations = $transportations;
|
||||
$this->receiptFile = $receiptFile;
|
||||
$this->contactEmail = $contactEmail;
|
||||
$this->contactPhone = $contactPhone;
|
||||
$this->accountOwner = $accountOwner;
|
||||
$this->accountIban = $accountIban;
|
||||
$this->totalAmount = $totalAmount;
|
||||
$this->isDonation = $isDonation;
|
||||
$this->userId = $userId;
|
||||
|
||||
if ($accountIban === 'undefined') {
|
||||
$this->accountIban = null;
|
||||
$this->accountOwner = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\CreateInvoice;
|
||||
|
||||
use App\Models\Invoice;
|
||||
|
||||
class CreateInvoiceResponse {
|
||||
public bool $success;
|
||||
public ?Invoice $invoice;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
$this->invoice = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\CreateInvoiceReceipt;
|
||||
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Models\PageText;
|
||||
use App\Models\Tenant;
|
||||
use App\Providers\PdfMergeProvider;
|
||||
use App\Resources\InvoiceResource;
|
||||
use Dompdf\Dompdf;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use ZipArchive;
|
||||
|
||||
class CreateInvoiceReceiptCommand {
|
||||
private CreateInvoiceReceiptRequest $request;
|
||||
private string $tempDirectory;
|
||||
|
||||
public function __construct(CreateInvoiceReceiptRequest $request) {
|
||||
$this->request = $request;
|
||||
$this->tempDirectory = Tenant::getTempDirectory();
|
||||
}
|
||||
|
||||
public function execute() : CreateInvoiceReceiptResponse {
|
||||
$response = new CreateInvoiceReceiptResponse();
|
||||
|
||||
$filename = $this->tempDirectory . $this->request->invoice->invoice_number . '.pdf';
|
||||
if (!Storage::exists($this->tempDirectory)) {
|
||||
Storage::makeDirectory(Tenant::getTempDirectory() . '/' . $this->tempDirectory);
|
||||
}
|
||||
|
||||
$receipt = $this->request->invoice->document_filename;
|
||||
if ($receipt === null) {
|
||||
Storage::put(
|
||||
$filename,
|
||||
$this->createPdf( $this->createHtml(), 'portrait', $filename, false )
|
||||
);
|
||||
|
||||
$response->fileName = $filename;
|
||||
return $response;
|
||||
}
|
||||
|
||||
$token = (string)rand(1000000000, 9999999999);
|
||||
$pdf_data = $this->createPdf( $this->createHtml(), 'portrait', $filename, false );
|
||||
|
||||
$tmpFileName = $this->tempDirectory . 'tmp-' . $token . '.pdf';
|
||||
Storage::put($tmpFileName, $pdf_data);
|
||||
|
||||
try {
|
||||
$merger = new PdfMergeProvider();
|
||||
|
||||
$merger
|
||||
->add( storage_path('app/private/' . $tmpFileName ))
|
||||
->add(storage_path('app/private/' . $receipt))
|
||||
->merge(storage_path('app/private/' .$filename) );
|
||||
|
||||
$response->fileName = $filename;
|
||||
} catch ( \Exception $e ) {
|
||||
$zip = new ZipArchive();
|
||||
$zip->open(
|
||||
storage_path('app/private/' .$filename) . '.zip',
|
||||
ZipArchive::CREATE | ZipArchive::OVERWRITE
|
||||
);
|
||||
|
||||
$zip->addFile(storage_path('app/private/' . $tmpFileName ), 'antrag.pdf');
|
||||
$zip->addFile(storage_path('app/private/' . $receipt), 'beleg.pdf');
|
||||
|
||||
$zip->close();
|
||||
$response->fileName = $filename . '.zip';
|
||||
|
||||
|
||||
} finally {
|
||||
Storage::delete($tmpFileName);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function createHtml() : string {
|
||||
|
||||
|
||||
$invoiceReadable = new InvoiceResource($this->request->invoice)->toArray();
|
||||
|
||||
|
||||
$travelPartTemplate = <<<HTML
|
||||
<tr><td>Reiseweg:</td><td>%1\$s</td></tr>
|
||||
<tr><td>Gesamtlänge der Strecke:</td><td>%2\$s km x %3\$s / km</td></tr>
|
||||
<tr><td>Materialtransport:</td><td>%4\$s</td></tr>
|
||||
<tr><td>Mitfahrende im PKW:</td><td>%5\$s</td></tr>
|
||||
HTML;
|
||||
|
||||
$flatTravelPart = sprintf(
|
||||
$travelPartTemplate,
|
||||
$invoiceReadable['travelDirection'] ,
|
||||
$invoiceReadable['distance'],
|
||||
$invoiceReadable['distanceAllowance'],
|
||||
$invoiceReadable['transportation'],
|
||||
$invoiceReadable['passengers']
|
||||
);
|
||||
|
||||
$invoiceTravelPart = '<tr><td>Kosten für ÖPNV:</td><td>' . $invoiceReadable['amount'] . '</td></tr>';;
|
||||
$expensePart = '<tr><td>Auslagenerstattung:</td><td>' . $invoiceReadable['amount'] . '</td></tr>';
|
||||
|
||||
$content = <<<HTML
|
||||
<html>
|
||||
<body style="margin-left: 20mm; margin-top: 17mm">
|
||||
<h3>Abrechnungstyp %1\$s</h3><br /><br />
|
||||
|
||||
|
||||
<table style="width: 100%%;">
|
||||
<tr><td>Abrechnungsnummer:</td><td>%2\$s</td></tr>
|
||||
<tr><td>Name:</td><td>%3\$s</td></tr>
|
||||
<tr><td>E-Mail:</td><td>%4\$s</td></tr>
|
||||
<tr><td>Telefon:</td><td>%5\$s</td></tr>
|
||||
<tr><td>Name der Kostenstelle:</td><td>%6\$s</td></tr>
|
||||
<tr><td>Zahlungsgrund:</td><td>%7\$s</td></tr>
|
||||
<tr><td>Wird der Betrag gespendet:</td><td>%8\$s</td></tr>
|
||||
%9\$s
|
||||
<tr style="font-weight: bold;">
|
||||
<td style="border-bottom-width: 1px; border-bottom-style: double;">
|
||||
Gesamtbetrag:
|
||||
</td>
|
||||
<td style="border-bottom-width: 1px; border-bottom-style: double;">
|
||||
%10\$s
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan="2"><br /><br /></td></tr>
|
||||
%11\$s
|
||||
<tr><td>Beleg digital eingereicht am:</td><td>%12\$s</td></tr>
|
||||
<tr><td>Beleg akzeptiert am:</td><td>%13\$s</td></tr>
|
||||
<tr><td>Beleg akzeptiert von:</td><td>%14\$s</td></tr>
|
||||
</table>
|
||||
%15\$s
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
|
||||
switch ($this->request->invoice->type) {
|
||||
case InvoiceType::INVOICE_TYPE_TRAVELLING:
|
||||
$paymentType = $this->request->invoice->distance !== null ? $flatTravelPart : $invoiceTravelPart;
|
||||
break;
|
||||
default:
|
||||
$paymentType = $expensePart;
|
||||
}
|
||||
|
||||
if ($this->request->invoice->donation) {
|
||||
$paymentInformation = '<tr><td colspan="2">' . PageText::where('name', 'CONFIRMATION_DONATE')->first()->content . '</td></tr>';
|
||||
} else {
|
||||
if ($this->request->invoice->contact_bank_iban === null) {
|
||||
$paymentInformation = '';
|
||||
} else {
|
||||
$paymentInformationTemplate = <<<HTML
|
||||
<tr><td colspan="2">%1\$s</td></tr>
|
||||
<tr><td>Kontoinhaber*in:</td><td>%2\$s</td></tr>
|
||||
<tr><td>INAN:</td><td>%3\$s</td></tr>
|
||||
<tr><td colspan="2"><br /><br /></td></tr>
|
||||
HTML;
|
||||
|
||||
$paymentInformation = sprintf(
|
||||
$paymentInformationTemplate,
|
||||
PageText::where('name', 'CONFIRMATION_PAYMENT')->first()->content,
|
||||
$invoiceReadable['accountOwner'],
|
||||
$invoiceReadable['accountIban']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$changes = $this->request->invoice->changes !== null ? '<p>' . $this->request->invoice->changes . '</p>' : '';
|
||||
|
||||
|
||||
return sprintf(
|
||||
$content,
|
||||
$invoiceReadable['invoiceTypeShort'],
|
||||
$invoiceReadable['invoiceNumber'],
|
||||
$invoiceReadable['contactName'],
|
||||
$invoiceReadable['contactEmail'],
|
||||
$invoiceReadable['contactPhone'],
|
||||
$invoiceReadable['costUnitName'],
|
||||
$invoiceReadable['invoiceType'],
|
||||
$invoiceReadable['donationText'],
|
||||
$paymentType,
|
||||
$invoiceReadable['amount'],
|
||||
$paymentInformation,
|
||||
$invoiceReadable['createdAt'],
|
||||
$invoiceReadable['approvedAt'],
|
||||
$invoiceReadable['approvedBy'],
|
||||
$changes
|
||||
);
|
||||
}
|
||||
|
||||
private function createPdf( string $htmlfile, string $orientation, string $filename, bool $download = true ) {
|
||||
|
||||
$dompdf = new Dompdf();
|
||||
$dompdf->loadHtml( $htmlfile, 'UTF-8' );
|
||||
$dompdf->setPaper( 'A4', $orientation );
|
||||
|
||||
$dompdf->render();
|
||||
if ( ! $download ) {
|
||||
return $dompdf->output();
|
||||
}
|
||||
$dompdf->stream( $filename );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\CreateInvoiceReceipt;
|
||||
|
||||
use App\Models\Invoice;
|
||||
|
||||
class CreateInvoiceReceiptRequest {
|
||||
public Invoice $invoice;
|
||||
|
||||
public function __construct(Invoice $invoice) {
|
||||
$this->invoice = $invoice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\CreateInvoiceReceipt;
|
||||
|
||||
class CreateInvoiceReceiptResponse {
|
||||
public string $fileName;
|
||||
|
||||
public function __construct() {
|
||||
$this->fileName = '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\UpdateInvoice;
|
||||
|
||||
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
|
||||
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
use App\ValueObjects\Amount;
|
||||
|
||||
class UpdateInvoiceCommand {
|
||||
private UpdateInvoiceRequest $request;
|
||||
|
||||
public function __construct(UpdateInvoiceRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : UpdateInvoiceResponse {
|
||||
$response = new UpdateInvoiceResponse();
|
||||
$changes = $this->request->invoice->changes ?? '';
|
||||
|
||||
if ($this->request->invoice->amount !== $this->request->amount->getAmount()) {
|
||||
$changes .= 'Betrag geändert von ' . Amount::fromString($this->request->invoice->amount)->toString() . ' auf ' . Amount::fromString($this->request->amount->getAmount())->toString() . '.<br />';
|
||||
$this->request->invoice->amount = $this->request->amount->getAmount();
|
||||
}
|
||||
|
||||
if ($this->request->invoice->invoiceType()->slug !== $this->request->invoiceType->slug) {
|
||||
$changes .= 'Abrechnungstyp geändert von ' . $this->request->invoice->invoiceType()->name . ' auf ' . $this->request->invoiceType->name . '.<br />';
|
||||
$this->request->invoice->type = $this->request->invoiceType->slug;
|
||||
}
|
||||
|
||||
if ($this->request->invoice->costUnit()->first()->id !== $this->request->costUnit->id) {
|
||||
$changes .= 'Kostenstelle geändert von ' . $this->request->invoice->costUnit()->first()->name . ' auf ' . $this->request->costUnit->name . '.<br />';
|
||||
$this->request->invoice->cost_unit_id = $this->request->costUnit->id;
|
||||
}
|
||||
|
||||
|
||||
$this->request->invoice->comment = $this->request->comment;
|
||||
$this->request->invoice->changes = $changes;
|
||||
|
||||
$this->request->invoice->save();
|
||||
|
||||
$request = new ChangeStatusRequest($this->request->invoice, InvoiceStatus::INVOICE_STATUS_APPROVED);
|
||||
$changeStatusCommand = new ChangeStatusCommand($request);
|
||||
$changeStatusCommand->execute();
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\UpdateInvoice;
|
||||
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Models\CostUnit;
|
||||
use App\Models\Invoice;
|
||||
use App\ValueObjects\Amount;
|
||||
|
||||
class UpdateInvoiceRequest {
|
||||
public string $comment;
|
||||
public InvoiceType $invoiceType;
|
||||
public CostUnit $costUnit;
|
||||
public Invoice $invoice;
|
||||
public Amount $amount;
|
||||
|
||||
public function __construct(Invoice $invoice, string $comment, InvoiceType $invoiceType, CostUnit $costUnit, Amount $amount) {
|
||||
$this->comment = $comment;
|
||||
$this->invoiceType = $invoiceType;
|
||||
$this->costUnit = $costUnit;
|
||||
$this->invoice = $invoice;
|
||||
$this->amount = $amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\UpdateInvoice;
|
||||
|
||||
use App\Models\Invoice;
|
||||
|
||||
class UpdateInvoiceResponse {
|
||||
public bool $success;
|
||||
public ?Invoice $invoice;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
$this->invoice = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\UploadInvoice;
|
||||
|
||||
use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptCommand;
|
||||
use App\Domains\Invoice\Actions\CreateInvoiceReceipt\CreateInvoiceReceiptRequest;
|
||||
use App\Providers\WebDavProvider;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UploadInvoiceCommand {
|
||||
private UploadInvoiceRequest $request;
|
||||
public function __construct(UploadInvoiceRequest $request) {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function execute() : UploadInvoiceResponse {
|
||||
$uploadResponse = new UploadInvoiceResponse();
|
||||
|
||||
$uploadDir = sprintf(
|
||||
'%1$s%2$s/%3$s',
|
||||
WebDavProvider::INVOICE_PREFIX,
|
||||
app('tenant')->url,
|
||||
$this->request->invoice->costUnit()->first()->name
|
||||
);
|
||||
|
||||
$webDavProvider = new WebDavProvider($uploadDir);
|
||||
|
||||
$createInvoiceReceiptRequest = new CreateInvoiceReceiptRequest($this->request->invoice);
|
||||
$createInvoiceReceiptCommand = new CreateInvoiceReceiptCommand($createInvoiceReceiptRequest);
|
||||
$response = $createInvoiceReceiptCommand->execute();
|
||||
if ('' === $response->fileName) {
|
||||
app('taskLogger')->error('PDF oder ZIP zur Abrechnung konnte nicht erstellt werden.');
|
||||
return $uploadResponse;
|
||||
}
|
||||
|
||||
if ($webDavProvider->uploadFile($response->fileName)) {
|
||||
$this->request->invoice->upload_required = false;
|
||||
$this->request->invoice->save();
|
||||
$uploadResponse->success = true;
|
||||
} else {
|
||||
app('taskLogger')->error('PDF oder ZIP zur Abrechnung konnte nicht hochgeladen werden.');
|
||||
}
|
||||
|
||||
if (Storage::exists($response->fileName)) {
|
||||
Storage::delete($response->fileName);
|
||||
}
|
||||
|
||||
return $uploadResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\UploadInvoice;
|
||||
|
||||
use App\Models\Invoice;
|
||||
|
||||
class UploadInvoiceRequest {
|
||||
public Invoice $invoice;
|
||||
|
||||
public function __construct(Invoice $invoice) {
|
||||
$this->invoice = $invoice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Actions\UploadInvoice;
|
||||
|
||||
class UploadInvoiceResponse {
|
||||
public bool $success;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
}
|
||||
}
|
||||
27
app/Domains/Invoice/Controllers/ChangeStateController.php
Normal file
27
app/Domains/Invoice/Controllers/ChangeStateController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Controllers;
|
||||
|
||||
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
|
||||
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ChangeStateController extends CommonController
|
||||
{
|
||||
public function __invoke(int $invoiceId, string $newState) : JsonResponse {
|
||||
$invoice = $this->invoices->getAsTreasurer($invoiceId);
|
||||
if ($invoice === null) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$comment = request()->get('reason') ?? null;
|
||||
$changeStatusRequest = new ChangeStatusRequest($invoice, $newState, $comment);
|
||||
$changeStatusCommand = new ChangeStatusCommand($changeStatusRequest);
|
||||
if ($changeStatusCommand->execute()->success) {
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
||||
136
app/Domains/Invoice/Controllers/EditController.php
Normal file
136
app/Domains/Invoice/Controllers/EditController.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Controllers;
|
||||
|
||||
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusCommand;
|
||||
use App\Domains\Invoice\Actions\ChangeStatus\ChangeStatusRequest;
|
||||
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceCommand;
|
||||
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceRequest;
|
||||
use App\Domains\Invoice\Actions\UpdateInvoice\UpdateInvoiceCommand;
|
||||
use App\Domains\Invoice\Actions\UpdateInvoice\UpdateInvoiceRequest;
|
||||
use App\Enumerations\CostUnitType;
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Resources\InvoiceResource;
|
||||
use App\Scopes\CommonController;
|
||||
use App\ValueObjects\Amount;
|
||||
use App\ValueObjects\InvoiceFile;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EditController extends CommonController{
|
||||
public function copyInvoice(int $invoiceId) : JsonResponse{
|
||||
$invoice = $this->invoices->getAsTreasurer($invoiceId);
|
||||
if ($invoice === null) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$receiptfile = null;
|
||||
if ($invoice->document_filename !== null) {
|
||||
$receiptfile = new InvoiceFile();
|
||||
$receiptfile->filename = $invoice->document_filename;
|
||||
$receiptfile->fullPath = $invoice->document_filename;
|
||||
}
|
||||
$createInvoiceRequest = new CreateInvoiceRequest(
|
||||
$invoice->costUnit()->first(),
|
||||
$invoice->contact_name,
|
||||
$invoice->type,
|
||||
$invoice->amount,
|
||||
$receiptfile,
|
||||
$invoice->donation,
|
||||
$invoice->user_id,
|
||||
$invoice->contact_email,
|
||||
$invoice->contact_phone,
|
||||
$invoice->contact_bank_owner,
|
||||
$invoice->contact_bank_iban,
|
||||
$invoice->type_other,
|
||||
$invoice->travel_direction,
|
||||
$invoice->distance,
|
||||
$invoice->passengers,
|
||||
$invoice->transportation
|
||||
);
|
||||
|
||||
$invoiceCreationCommand = new CreateInvoiceCommand($createInvoiceRequest);
|
||||
$newInvoice = $invoiceCreationCommand->execute()->invoice;
|
||||
|
||||
$invoiceDenyRequest = new ChangeStatusRequest($invoice,InvoiceStatus::INVOICE_STATUS_DENIED, 'Abrechnungskorrektur in Rechnungsnummer #' . $newInvoice->invoice_number . ' erstellt.');
|
||||
$invoiceDenyCommand = new ChangeStatusCommand($invoiceDenyRequest);
|
||||
$invoiceDenyCommand->execute();
|
||||
|
||||
|
||||
$runningJobs = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_RUNNING_JOB);
|
||||
$currentEvents = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_EVENT);
|
||||
|
||||
return response()->json([
|
||||
'invoice' => new InvoiceResource($invoice)->toArray(),
|
||||
'status' => 'success',
|
||||
'costUnits' => array_merge($runningJobs, $currentEvents),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateInvoice(int $invoiceId, Request $request) : JsonResponse {
|
||||
$invoice = $this->invoices->getAsTreasurer($invoiceId);
|
||||
if ($invoice === null) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$modifyData = $request->get('invoiceData');
|
||||
|
||||
$newAmount = Amount::fromString($modifyData['amount']);
|
||||
$amountLeft = Amount::fromString($invoice->amount);
|
||||
$amountLeft->subtractAmount($newAmount);
|
||||
|
||||
$newCostUnit = $this->costUnits->getById($modifyData['cost_unit'],true);
|
||||
$invoiceType = InvoiceType::where('slug', $modifyData['type_internal'])->first();
|
||||
|
||||
|
||||
$updateInvoiceRequest = new UpdateInvoiceRequest(
|
||||
$invoice,
|
||||
$modifyData['reason_of_correction'] ?? 'Abrechnungskorrektur',
|
||||
$invoiceType,
|
||||
$newCostUnit,
|
||||
$newAmount
|
||||
);
|
||||
$updateInvoiceCommand = new UpdateInvoiceCommand($updateInvoiceRequest);
|
||||
$updateInvoiceCommand->execute();
|
||||
|
||||
|
||||
$newInvoice = null;
|
||||
if (isset($modifyData['duplicate']) && $modifyData['duplicate'] === true) {
|
||||
$receiptfile = null;
|
||||
if ($invoice->document_filename !== null) {
|
||||
$receiptfile = new InvoiceFile();
|
||||
$receiptfile->filename = $invoice->document_filename;
|
||||
$receiptfile->fullPath = $invoice->document_filename;
|
||||
}
|
||||
$createInvoiceRequest = new CreateInvoiceRequest(
|
||||
$invoice->costUnit()->first(),
|
||||
$invoice->contact_name,
|
||||
$invoice->type,
|
||||
$amountLeft->getAmount(),
|
||||
$receiptfile,
|
||||
$invoice->donation,
|
||||
$invoice->user_id,
|
||||
$invoice->contact_email,
|
||||
$invoice->contact_phone,
|
||||
$invoice->contact_bank_owner,
|
||||
$invoice->contact_bank_iban,
|
||||
$invoice->type_other,
|
||||
$invoice->travel_direction,
|
||||
$invoice->distance,
|
||||
$invoice->passengers,
|
||||
$invoice->transportation
|
||||
);
|
||||
|
||||
$invoiceCreationCommand = new CreateInvoiceCommand($createInvoiceRequest);
|
||||
$newInvoice = $invoiceCreationCommand->execute()->invoice;
|
||||
}
|
||||
|
||||
$useInvoice = $newInvoice ?? $invoice;
|
||||
$do_copy = $newInvoice !== null ? true : false;
|
||||
return response()->json([
|
||||
'invoice' => new InvoiceResource($useInvoice)->toArray(),
|
||||
'do_copy' => $do_copy,
|
||||
]);
|
||||
}
|
||||
}
|
||||
61
app/Domains/Invoice/Controllers/ListMyInvoicesController.php
Normal file
61
app/Domains/Invoice/Controllers/ListMyInvoicesController.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Controllers;
|
||||
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ListMyInvoicesController extends CommonController {
|
||||
function __invoke(string $invoiceStatus) {
|
||||
$invoices = $this->invoices->getMyInvoicesByStatus($invoiceStatus);
|
||||
|
||||
$subTabIndex = 0;
|
||||
switch ($invoiceStatus) {
|
||||
case InvoiceStatus::INVOICE_STATUS_NEW:
|
||||
$subTabIndex = 0;
|
||||
break;
|
||||
case InvoiceStatus::INVOICE_STATUS_APPROVED:
|
||||
$subTabIndex = 1;
|
||||
break;
|
||||
case InvoiceStatus::INVOICE_STATUS_DENIED:
|
||||
$subTabIndex = 2;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
|
||||
$inertiaProvider = new InertiaProvider('Invoice/ListMyInvoices', [
|
||||
'invoices' => $invoices,
|
||||
'endpoint' => $invoiceStatus,
|
||||
'currentStatus' => $subTabIndex,
|
||||
]);
|
||||
return $inertiaProvider->render();
|
||||
}
|
||||
|
||||
public function getMyInvoicesByStatus(string $invoiceStatus) : JsonResponse {
|
||||
$invoices = $this->invoices->getMyInvoicesByStatus($invoiceStatus);
|
||||
|
||||
$title = '';
|
||||
switch ($invoiceStatus) {
|
||||
case InvoiceStatus::INVOICE_STATUS_NEW:
|
||||
$title = 'Neue Abrechnungen';
|
||||
break;
|
||||
case InvoiceStatus::INVOICE_STATUS_APPROVED:
|
||||
$title = 'Freigegebene Abrechnungen, nicht exportierte Abrechnungen';
|
||||
break;
|
||||
case InvoiceStatus::INVOICE_STATUS_DENIED:
|
||||
$title = 'Abgelehnte Abrechnungen';
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'title' => $title,
|
||||
'endpoint' => $invoiceStatus,
|
||||
'invoices' => $invoices,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
138
app/Domains/Invoice/Controllers/NewInvoiceController.php
Normal file
138
app/Domains/Invoice/Controllers/NewInvoiceController.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Controllers;
|
||||
|
||||
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceCommand;
|
||||
use App\Domains\Invoice\Actions\CreateInvoice\CreateInvoiceRequest;
|
||||
use App\Enumerations\CostUnitType;
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Models\CostUnit;
|
||||
use App\Providers\FlashMessageProvider;
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Providers\UploadFileProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use App\ValueObjects\Amount;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NewInvoiceController extends CommonController {
|
||||
public function __invoke() {
|
||||
$userData = $this->users->getCurrentUserDetails();
|
||||
|
||||
$runningJobs = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_RUNNING_JOB);
|
||||
$currentEvents = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_EVENT);
|
||||
|
||||
|
||||
$inertiaProvider = new InertiaProvider('Invoice/NewInvoice', [
|
||||
'userName' => $userData['userName'],
|
||||
'userEmail' => $userData['userEmail'],
|
||||
'userTelephone' => $userData['userTelephone'],
|
||||
'userAccountOwner' => $userData['userAccountOwner'],
|
||||
'userAccountIban' => $userData['userAccountIban'],
|
||||
'runningJobs' => $runningJobs,
|
||||
'currentEvents' => $currentEvents,
|
||||
]);
|
||||
return $inertiaProvider->render();
|
||||
}
|
||||
|
||||
public function saveInvoice(Request $request, int $costUnitId, string $invoiceType) : JsonResponse {
|
||||
$costUnit = $this->costUnits->getById($costUnitId, true);
|
||||
if (null === $costUnit) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Beim Speichern ist ein Fehler aufgetreten. Bitte starte den Vorgang erneut.'
|
||||
]);
|
||||
}
|
||||
|
||||
$uploadedFile = null;
|
||||
if (null !== $request->file('receipt')) {
|
||||
$validation = sprintf('%1$s|%2$s|max:%3$s',
|
||||
'required',
|
||||
'mimes:pdf',
|
||||
env('MAX_INVOICE_FILE_SIZE', 16)*10
|
||||
);
|
||||
|
||||
$request->validate([
|
||||
'receipt' => $validation
|
||||
]);
|
||||
|
||||
$uploadFileProvider = new UploadFileProvider($request->file('receipt'), $costUnit);
|
||||
$uploadedFile = $uploadFileProvider->saveUploadedFile();
|
||||
}
|
||||
|
||||
switch ($invoiceType) {
|
||||
case InvoiceType::INVOICE_TYPE_TRAVELLING:
|
||||
|
||||
if ($uploadedFile !== null) {
|
||||
$amount = Amount::fromString($request->get('amount'))->getAmount();
|
||||
$distance = null;
|
||||
} else {
|
||||
$distance = Amount::fromString($request->get('amount'))->getRoundedAmount();
|
||||
$amount = $distance * $costUnit->distance_allowance;
|
||||
|
||||
}
|
||||
|
||||
$createInvoiceRequest = new CreateInvoiceRequest(
|
||||
$costUnit,
|
||||
$request->get('name'),
|
||||
InvoiceType::INVOICE_TYPE_TRAVELLING,
|
||||
$amount,
|
||||
$uploadedFile,
|
||||
'donation' === $request->get('decision') ? true : false,
|
||||
$this->users->getCurrentUserDetails()['userId'],
|
||||
$request->get('contactEmail'),
|
||||
$request->get('telephone'),
|
||||
$request->get('accountOwner'),
|
||||
$request->get('accountIban'),
|
||||
null,
|
||||
$request->get('otherText'),
|
||||
$distance,
|
||||
$request->get('havePassengers'),
|
||||
$request->get('materialTransportation'),
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$createInvoiceRequest = new CreateInvoiceRequest(
|
||||
$costUnit,
|
||||
$request->get('name'),
|
||||
$invoiceType,
|
||||
Amount::fromString($request->get('amount'))->getAmount(),
|
||||
$uploadedFile,
|
||||
'donation' === $request->get('decision') ? true : false,
|
||||
$this->users->getCurrentUserDetails()['userId'],
|
||||
$request->get('contactEmail'),
|
||||
$request->get('telephone'),
|
||||
$request->get('accountOwner'),
|
||||
$request->get('accountIban'),
|
||||
$request->get('otherText'),
|
||||
null,
|
||||
null,
|
||||
$request->get('havePassengers'),
|
||||
$request->get('materialTransportation'),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$command = new CreateInvoiceCommand($createInvoiceRequest);
|
||||
$response = $command->execute();
|
||||
if ($response->success) {
|
||||
new FlashMessageProvider(
|
||||
'Die Abrechnung wurde erfolgreich angelegt.' . PHP_EOL . PHP_EOL . 'Sollten wir Rückfragen haben, melden wir uns bei dir',
|
||||
'success'
|
||||
);
|
||||
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Alright'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
dd($request->all());
|
||||
}
|
||||
}
|
||||
55
app/Domains/Invoice/Controllers/ShowInvoiceController.php
Normal file
55
app/Domains/Invoice/Controllers/ShowInvoiceController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Invoice\Controllers;
|
||||
|
||||
use App\Enumerations\CostUnitType;
|
||||
use App\Resources\InvoiceResource;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class ShowInvoiceController extends CommonController {
|
||||
public function __invoke(int $invoiceId) : JsonResponse{
|
||||
$invoice = $this->invoices->getAsTreasurer($invoiceId);
|
||||
|
||||
$runningJobs = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_RUNNING_JOB);
|
||||
$currentEvents = $this->costUnits->getCostUnitsForNewInvoice(CostUnitType::COST_UNIT_TYPE_EVENT);
|
||||
|
||||
return response()->json([
|
||||
'invoice' => new InvoiceResource($invoice)->toArray(),
|
||||
'costUnits' => array_merge($runningJobs, $currentEvents),
|
||||
]);
|
||||
}
|
||||
|
||||
public function showReceipt(int $invoiceId): BinaryFileResponse
|
||||
{
|
||||
$invoice = $this->invoices->getAsTreasurer($invoiceId);
|
||||
if (null === $invoice) {
|
||||
abort(404, 'Datei nicht gefunden');
|
||||
}
|
||||
|
||||
if (null === $invoice->document_filename) {
|
||||
abort(404, 'Datei nicht gefunden');
|
||||
}
|
||||
|
||||
$path = $invoice->document_filename;
|
||||
// Pfad zur Datei
|
||||
$fullPath = 'private/' . $path;
|
||||
|
||||
|
||||
|
||||
|
||||
if (!Storage::exists($path)) {
|
||||
|
||||
|
||||
|
||||
abort(404, 'Datei nicht gefunden');
|
||||
}
|
||||
|
||||
return response()->file(storage_path('app/' . $fullPath), [
|
||||
'Content-Type' => 'application/pdf'
|
||||
]);
|
||||
}
|
||||
}
|
||||
34
app/Domains/Invoice/Routes/api.php
Normal file
34
app/Domains/Invoice/Routes/api.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\CostUnit\Controllers\DistanceAllowanceController;
|
||||
use App\Domains\CostUnit\Controllers\ListController;
|
||||
use App\Domains\Invoice\Controllers\ChangeStateController;
|
||||
use App\Domains\Invoice\Controllers\EditController;
|
||||
use App\Domains\Invoice\Controllers\ListMyInvoicesController;
|
||||
use App\Domains\Invoice\Controllers\NewInvoiceController;
|
||||
use App\Domains\Invoice\Controllers\ShowInvoiceController;
|
||||
use App\Middleware\IdentifyTenant;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
Route::prefix('api/v1/invoice')->group(function () {
|
||||
Route::post('/new/{costUnitId}/{invoiceType}', [NewInvoiceController::class, 'saveInvoice']);
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/details/{invoiceId}', ShowInvoiceController::class);
|
||||
Route::get('/showReceipt/{invoiceId}', [ShowInvoiceController::class, 'showReceipt']);
|
||||
|
||||
Route::post('/details/{invoiceId}/change-state/{newState}', ChangeStateController::class);
|
||||
Route::post('/details/{invoiceId}/copy', [EditController::class, 'copyInvoice']);
|
||||
Route::post('/details/{invoiceId}/update', [EditController::class, 'updateInvoice']);
|
||||
Route::get('/my-invoices/{invoiceStatus}', [ListMyInvoicesController::class, 'getMyInvoicesByStatus']);
|
||||
|
||||
|
||||
|
||||
|
||||
Route::get('/create', [CreateController::class, 'showForm']);
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
20
app/Domains/Invoice/Routes/web.php
Normal file
20
app/Domains/Invoice/Routes/web.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
use App\Domains\CostUnit\Controllers\OpenController;
|
||||
use App\Domains\Invoice\Controllers\ListMyInvoicesController;
|
||||
use App\Domains\Invoice\Controllers\NewInvoiceController;
|
||||
use App\Middleware\IdentifyTenant;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
Route::prefix('invoice')->group(function () {
|
||||
Route::get('/new', NewInvoiceController::class);
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/my-invoices/{invoiceStatus}', ListMyInvoicesController::class);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
<script setup>
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
|
||||
|
||||
const props = defineProps({
|
||||
navbar: Object,
|
||||
tenant: String,
|
||||
user: Object,
|
||||
currentPath: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title='Neue Abrechnung' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
|
||||
<!-- Alles hier wird in den Slot von AppLayout eingefügt -->
|
||||
<h2>Dashboard Content</h2>
|
||||
<p>Test 1!
|
||||
Hier wird mal eine Rechnung erstellt.
|
||||
Wenn es geht oder auch nicht</p>
|
||||
{{props.tenant}}
|
||||
|
||||
<button @click="$toast.success('Hallo vom Dashboard!')">Test Toaster</button>
|
||||
<button @click="$toast.error('Soe sieht ein Fehler aus')">Error Toaster</button>
|
||||
</AppLayout>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
58
app/Domains/Invoice/Views/ListMyInvoices.vue
Normal file
58
app/Domains/Invoice/Views/ListMyInvoices.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import {reactive, inject, onMounted} from 'vue';
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
|
||||
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
import ListInvoices from "./Partials/myInvoiceDetails/ListInvoices.vue";
|
||||
|
||||
const props = defineProps({
|
||||
currentStatus: Number,
|
||||
})
|
||||
|
||||
console.log(props.currentStatus)
|
||||
const initialCostUnitId = props.cost_unit_id
|
||||
const initialInvoiceId = props.invoice_id
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: 'Neue Abrechnungen',
|
||||
component: ListInvoices,
|
||||
endpoint: "/api/v1/invoice/my-invoices/new",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Freigegebene Abrechnungen',
|
||||
component: ListInvoices,
|
||||
endpoint: "/api/v1/invoice/my-invoices/approved",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Abgelehnte Abrechnungen',
|
||||
component: ListInvoices,
|
||||
endpoint: "/api/v1/invoice/my-invoices/denied",
|
||||
deep_jump_id: initialCostUnitId,
|
||||
deep_jump_id_sub: initialInvoiceId,
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (undefined !== props.message) {
|
||||
toast.success(props.message)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Meine Abrechnungen">
|
||||
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
|
||||
<tabbed-page :tabs="tabs" :subTabIndex=props.currentStatus />
|
||||
|
||||
</shadowed-box>
|
||||
</AppLayout>
|
||||
</template>
|
||||
79
app/Domains/Invoice/Views/NewInvoice.vue
Normal file
79
app/Domains/Invoice/Views/NewInvoice.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
|
||||
import TextResource from "../../../Views/Components/TextResource.vue";
|
||||
import Modal from "../../../Views/Components/Modal.vue";
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import ExpenseAccounting from "./Partials/newInvoice/expense-accounting.vue";
|
||||
import TravelExpenseAccounting from "./Partials/newInvoice/travel-expense-accounting.vue";
|
||||
|
||||
const props = defineProps({
|
||||
currentEvents: Object,
|
||||
runningJobs: Object,
|
||||
userId: Number,
|
||||
userName: String,
|
||||
userEmail: String,
|
||||
userTelephone: String,
|
||||
userIban: String,
|
||||
userAccountOwner: String,
|
||||
})
|
||||
|
||||
const isOpen = ref(false)
|
||||
const eventId = ref(0);
|
||||
const invoiceType = ref('');
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title='Neue Abrechnung'>
|
||||
<div class="invoice-main-flexbox" v-if="eventId === 0">
|
||||
<div
|
||||
class="invoice-type-layer"
|
||||
@click="isOpen = true;invoiceType='expense-accounting'"
|
||||
>
|
||||
<TextResource textName="NEW_COMMON_COST_EXPENSE_DESCRIPTION" />
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="invoice-type-layer"
|
||||
@click="isOpen = true;invoiceType='travel-expense-accounting'"
|
||||
>
|
||||
<TextResource textName="NEW_TRAVEL_COST_EXPENSE_DESCRIPTION" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ExpenseAccounting v-if="invoiceType === 'expense-accounting' && eventId !== 0"
|
||||
:eventId="eventId"
|
||||
:userName="props.userName"
|
||||
:userEmail="props.userEmail"
|
||||
:userTelephone="props.userTelephone"
|
||||
:userIban="props.userIban"
|
||||
:userAccountOwner="props.userAccountOwner"
|
||||
:userId="props.userId"
|
||||
/>
|
||||
|
||||
<TravelExpenseAccounting v-else-if="invoiceType === 'travel-expense-accounting' && eventId !== 0"
|
||||
:eventId="eventId"
|
||||
:userName="props.userName"
|
||||
:userEmail="props.userEmail"
|
||||
:userTelephone="props.userTelephone"
|
||||
:userIban="props.userIban"
|
||||
:userAccountOwner="props.userAccountOwner"
|
||||
:userId="props.userId"
|
||||
/>
|
||||
|
||||
<Modal :show="isOpen" title="Veranstaltung auswählen" @close="isOpen = false">
|
||||
<select v-model="eventId" @change="isOpen=false" style="width: 100%">
|
||||
<option value="0" disabled>Bitte auswählen</option>
|
||||
<optgroup label="Laufende Tätigkeiten">
|
||||
<option :value="event.id" v-for="event in props.runningJobs">{{ event.name }}</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="Veranstaltungen">
|
||||
<option :value="event.id" v-for="event in props.currentEvents">{{ event.name }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</Modal>
|
||||
</AppLayout>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
invoice: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="travel_allowance">
|
||||
<tr><td colspan="2">
|
||||
Abrechnung einer Reisekostenpauschale
|
||||
</td></tr>
|
||||
<tr>
|
||||
<th>Reiseroute</th>
|
||||
<td>{{props.invoice.travelRoute}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Gesamte Wegstrecke</th>
|
||||
<td>{{props.invoice.distance}} km</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Kilometerpauschale</th>
|
||||
<td>{{props.invoice.distanceAllowance}} Euro / km</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Gesamtbetrag</th>
|
||||
<td style="font-weight: bold">{{props.invoice.amount}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Marterialtransport</th>
|
||||
<td>{{props.invoice.transportation}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Hat Personen mitgenommen</th>
|
||||
<td>{{props.invoice.passengers}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.travel_allowance {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.travel_allowance tr th {
|
||||
width: 300px !important;
|
||||
border-left: 1px solid #ccc;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.travel_allowance tr td,
|
||||
.travel_allowance tr th {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.8em;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.travel_allowance tr td:last-child {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.travel_allowance tr:first-child td:first-child {
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to bottom, #fff, #f6f7f7);
|
||||
border-top: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup>
|
||||
import PdfViewer from "../../../../../Views/Components/PdfViewer.vue";
|
||||
import DistanceAllowance from "./DistanceAllowance.vue";
|
||||
import {onMounted, reactive} from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
newInvoice: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
costUnits: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit', 'cancel'])
|
||||
|
||||
const formData = reactive({
|
||||
type_internal: props.newInvoice.internalType || '',
|
||||
cost_unit: props.newInvoice.costUnitId || '',
|
||||
amount: props.newInvoice.amountPlain || '',
|
||||
reason_of_correction: '',
|
||||
})
|
||||
|
||||
const submitForm = () => {
|
||||
emit('submit', formData)
|
||||
}
|
||||
|
||||
|
||||
const invoiceTypeCollection = reactive({
|
||||
invoiceTypes: {}
|
||||
});
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/core/retrieve-invoice-types-all');
|
||||
const data = await response.json();
|
||||
Object.assign(invoiceTypeCollection, data);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<table style="width: 100%; font-family: sans-serif;">
|
||||
<tr>
|
||||
<td style="width: 150px;">Rechnungstyp:</td>
|
||||
<td style="width: 450px;">
|
||||
<select v-model="formData.type_internal" class="width-half-full">
|
||||
<option v-for="invoiceType in invoiceTypeCollection.invoiceTypes" :value="invoiceType.slug">{{invoiceType.name}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Kostenstelle:</td>
|
||||
<td>
|
||||
<select v-model="formData.cost_unit" class="width-half-full">
|
||||
<option v-for="costUnit in props.costUnits" :value="costUnit.id">{{costUnit.name}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Gesamtbetrag:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.amount" class="width-small" /> Euro
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Grund der Korrektur:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.reason_of_correction" class="width-half-full" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="checkbox" v-model="formData.duplicate" id="mareike_correct_invoice_duplicate" />
|
||||
<label for="mareike_correct_invoice_duplicate">Kopie zur Weiterbearbeitung erstellen</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="submit" value="Speichern und freigeben" class="button mareike-accept-button" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<br /><br />
|
||||
|
||||
<PdfViewer :url="'/api/v1/invoice/showReceipt/' + props.newInvoice.id" v-if="props.newInvoice.documentFilename !== null" />
|
||||
<DistanceAllowance v-else :invoice="props.newInvoice" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.width-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.width-almost-full {
|
||||
width: calc(100% - 75px);
|
||||
}
|
||||
|
||||
.width-half-full {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.width-small {
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
120
app/Domains/Invoice/Views/Partials/invoiceDetails/Header.vue
Normal file
120
app/Domains/Invoice/Views/Partials/invoiceDetails/Header.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
modeShow: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(["accept", "deny", "fix", "reopen"])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span id="invoice_details_header">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name:</td>
|
||||
<td v-if="modeShow">{{props.data.contactName}}</td>
|
||||
<td v-else style="width: 300px;">{{props.data.contactName}}</td>
|
||||
|
||||
<td v-if="modeShow" style="width: 250px;">Kostenstelle</td>
|
||||
<td v-else style="width: 300px;">Kostensatelle (ursprünglich)</td>
|
||||
|
||||
<td>{{props.data.costUnitName}}</td>
|
||||
<td rowspan="4" style="width: 250px; padding-right: 25px; text-align: right; vertical-align: top;">
|
||||
<input type="button"
|
||||
v-if="props.data.status === 'new' && modeShow"
|
||||
@click="emit('accept')"
|
||||
class="accept-button"
|
||||
style="width: 100%; margin-bottom: 10px;"
|
||||
value="Abrechnung annehmen"
|
||||
/>
|
||||
|
||||
<input type="button" v-if="props.data.status === 'denied' && modeShow"
|
||||
@click="emit('reopen')"
|
||||
class="accept-button"
|
||||
style="width: 100%; margin-bottom: 10px;"
|
||||
value="Abrechnung zur Wiedervorlage öffnen"
|
||||
/>
|
||||
|
||||
<br />
|
||||
|
||||
<input type="button"
|
||||
v-if="props.data.status === 'new' && modeShow"
|
||||
@click="emit('fix')"
|
||||
class="fix-button"
|
||||
style="width: 100%; margin-bottom: 10px;"
|
||||
value="Abrechnung ablehnen und korrigieren"
|
||||
/>
|
||||
<br />
|
||||
|
||||
<input type="button"
|
||||
v-if="props.data.status === 'new' && modeShow"
|
||||
@click="emit('deny')"
|
||||
class="deny-button"
|
||||
style="width: 100%"
|
||||
value="Abrechnung ablehnen"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Rest der Tabelle bleibt unverändert -->
|
||||
<tr>
|
||||
<td>E-Mail:</td>
|
||||
<td>{{props.data.contactEmail}}</td>
|
||||
<td>
|
||||
Abrechnungsnummer
|
||||
<label v-if="!modeShow"> (ursprünglich)</label>:
|
||||
</td>
|
||||
|
||||
<td>{{props.data.invoiceNumber}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Telefon:</td>
|
||||
<td>{{props.data.contactPhone}}</td>
|
||||
<td>
|
||||
Abrechnungstyp
|
||||
<label v-if="!modeShow"> (Ursprünglich)</label>:
|
||||
</td>
|
||||
<td>{{props.data.invoiceType}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Kontoinhaber*in:</td>
|
||||
<td>{{props.data.accountOwner}}</td>
|
||||
<td>Gesamtbetrag
|
||||
<label v-if="!modeShow"> (Ursprünglich)</label>:
|
||||
</td>
|
||||
<td><strong>{{props.data.amount}}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IBAN:</td>
|
||||
<td>{{props.data.accountIban}}</td>
|
||||
<td>Buchungsinformationen:</td>
|
||||
<td v-if="props.data.donation">Als Spende gebucht</td>
|
||||
<td v-else-if="props.data.alreadyPaid">Beleg ohne Auszahlung</td>
|
||||
<td v-else>Klassische Auszahlung</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status:</td>
|
||||
<td>{{props.data.readableStatus}}</td>
|
||||
<td>Anmerkungen:</td>
|
||||
<td>
|
||||
<span v-if="props.data.status === 'denied'">
|
||||
{{props.data.deniedReason}}
|
||||
</span>
|
||||
<span v-else>{{props.data.comment}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,285 @@
|
||||
<script setup>
|
||||
|
||||
import FullScreenModal from "../../../../../Views/Components/FullScreenModal.vue";
|
||||
import {ref} from "vue";
|
||||
import Modal from "../../../../../Views/Components/Modal.vue";
|
||||
import { useAjax } from "../../../../../../resources/js/components/ajaxHandler.js";
|
||||
import ShowInvoicePartial from "./ShowInvoice.vue";
|
||||
import EditInvoicePartial from "./EditInvoice.vue";
|
||||
import Header from "./Header.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}, showInvoice: Boolean
|
||||
})
|
||||
const showInvoice = ref(props.showInvoice)
|
||||
const emit = defineEmits(["close"])
|
||||
const denyInvoiceDialog = ref(false)
|
||||
const { data, loading, error, request } = useAjax()
|
||||
const modeShow = ref(true)
|
||||
const costUnits = ref(null)
|
||||
const newInvoice = ref(null)
|
||||
|
||||
async function acceptInvoice() {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/approved", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (data.status === 'success') {
|
||||
toast.success('Abrechnung wurde freigegeben.');
|
||||
} else {
|
||||
toast.error('Bei der Bearbeitung ist ein Fehler aufgetreten.');
|
||||
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
function close() {
|
||||
emit('reload')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function updateInvoice(formData) {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/update", {
|
||||
method: "POST",
|
||||
body: {
|
||||
invoiceData: formData
|
||||
}
|
||||
});
|
||||
|
||||
if (!data.do_copy) {
|
||||
modeShow.value = true;
|
||||
toast.success('Die Koreektur der Abrechnung wurde gespeichert.');
|
||||
close();
|
||||
} else {
|
||||
modeShow.value = true;
|
||||
newInvoice.value = data.invoice;
|
||||
props.data.id = data.invoice.id;
|
||||
reloadInvoiceFixDialog()
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadInvoiceFixDialog() {
|
||||
const data = await request("api/v1/invoice/details/" + props.data.id, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
newInvoice.value = data.invoice;
|
||||
props.data.id = data.invoice.id;
|
||||
costUnits.value = data.costUnits;
|
||||
props.data.id = data.invoice.id;
|
||||
|
||||
modeShow.value = false;
|
||||
toast.success('Die Abrechnung wurde gespeichert und eine neue Abrechnung wurde erstellt.');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function openInvoiceFixDialog() {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/copy", {
|
||||
method: "POST",
|
||||
body: {
|
||||
}
|
||||
});
|
||||
|
||||
if (data.status === 'success') {
|
||||
costUnits.value = data.costUnits;
|
||||
newInvoice.value = data.invoice;
|
||||
props.data.id = data.invoice.id;
|
||||
modeShow.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function openDenyInvoiceDialog() {
|
||||
denyInvoiceDialog.value = true;
|
||||
}
|
||||
|
||||
async function denyInvoice() {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/denied", {
|
||||
method: "POST",
|
||||
body: {
|
||||
reason: document.getElementById('deny_invoice_reason').value
|
||||
}
|
||||
});
|
||||
|
||||
if (data.status === 'success') {
|
||||
toast.success('Abrechnung wurde abgelehnt.');
|
||||
} else {
|
||||
toast.error('Bei der Bearbeitung ist ein Fehler aufgetreten.');
|
||||
|
||||
}
|
||||
|
||||
denyInvoiceDialog.value = false;
|
||||
close();
|
||||
}
|
||||
|
||||
async function reopenInvoice() {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/new", {
|
||||
method: "POST",
|
||||
|
||||
});
|
||||
|
||||
if (data.status === 'success') {
|
||||
toast.success('Die Abrechnung wurde zur erneuten Bearbeitung vorgelegt');
|
||||
} else {
|
||||
toast.error('Beim Bearbeiten ist ein Fehler aufgetreten.');
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<FullScreenModal
|
||||
:show="showInvoice"
|
||||
title="Abrechnungsdetails"
|
||||
@close="emit('close')"
|
||||
>
|
||||
|
||||
<Header :data="props.data"
|
||||
@accept="acceptInvoice"
|
||||
@deny="openDenyInvoiceDialog"
|
||||
@fix="openInvoiceFixDialog"
|
||||
@reopen="reopenInvoice"
|
||||
:modeShow="modeShow"
|
||||
/>
|
||||
|
||||
<ShowInvoicePartial
|
||||
v-if="modeShow"
|
||||
:data="props.data"
|
||||
@accept="acceptInvoice"
|
||||
@deny="openDenyInvoiceDialog"
|
||||
@fix="openInvoiceFixDialog"
|
||||
/>
|
||||
|
||||
<EditInvoicePartial
|
||||
v-else
|
||||
:newInvoice="newInvoice"
|
||||
:costUnits="costUnits"
|
||||
@accept="acceptInvoice"
|
||||
@deny="openDenyInvoiceDialog"
|
||||
@fix="openInvoiceFixDialog"
|
||||
@update="updateInvoice"
|
||||
@submit="updateInvoice"
|
||||
/>
|
||||
|
||||
|
||||
</FullScreenModal>
|
||||
|
||||
<Modal title="Abrechnung ablehnen" :show="denyInvoiceDialog" @close="denyInvoiceDialog = false" >
|
||||
Begründung:
|
||||
<textarea class="mareike-textarea" style="width: 100%; height: 100px; margin-top: 10px;" id="deny_invoice_reason" />
|
||||
<input type="button" class="mareike-button mareike-deny-invoice-button" value="Abrechnung ablehnen" @click="denyInvoice" />
|
||||
</Modal>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.mareike-deny-invoice-button {
|
||||
width: 150px !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#invoice_details_header{
|
||||
font-weight: bold;
|
||||
font-size: 12pt;
|
||||
line-height: 1.8em;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
#invoice_details_header table {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 10px !important;
|
||||
width: 98%;
|
||||
border-color: #c0c0c0;
|
||||
box-shadow: 5px 5px 10px #c0c0c0;
|
||||
margin-bottom: 75px;
|
||||
font-weight: normal;
|
||||
}
|
||||
#invoice_details_header table tr td:first-child {
|
||||
padding-right: 50px;
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
#invoice_details_header table tr td:nth-child(2) {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
#invoice_details_header table tr td:nth-child(3) {
|
||||
padding-right: 50px;
|
||||
width: 100px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#invoice_details_body {
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#invoice_details_body table tr:nth-child(1) td,
|
||||
#invoice_details_body table tr:nth-child(2) td,
|
||||
#invoice_details_body table tr:nth-child(3) td,
|
||||
#invoice_details_body table tr:nth-child(4) td,
|
||||
#invoice_details_body table tr:nth-child(6) td{
|
||||
vertical-align: top;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#invoice_details_body table tr:nth-child(5) td {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
#invoice_details_body table {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
#invoice_details_body table tr:nth-child(1) td:first-child {
|
||||
padding-right: 50px;
|
||||
|
||||
}
|
||||
|
||||
#invoice_details_body table tr:nth-child(1) td:nth-child(2),
|
||||
#invoice_details_body table tr:nth-child(1) td:nth-child(3)
|
||||
{
|
||||
width: 250px;
|
||||
|
||||
}
|
||||
|
||||
.mareike-accept-button {
|
||||
background-color: #36c054 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.mareike-deny-button {
|
||||
background-color: #ee4b5c !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.mareike-fix-button {
|
||||
background-color: #d3d669 !important;
|
||||
color: #67683c !important;
|
||||
}
|
||||
|
||||
.mareike-button {
|
||||
padding: 5px 25px !important;
|
||||
font-size: 11pt !important;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import PdfViewer from "../../../../../Views/Components/PdfViewer.vue";
|
||||
import DistanceAllowance from "./DistanceAllowance.vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
console.log(props.data)
|
||||
const emit = defineEmits(["accept", "deny", "fix"])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span id="invoice_details_body">
|
||||
<PdfViewer :url="'/api/v1/invoice/showReceipt/' + props.data.id" v-if="props.data.documentFilename !== null" />
|
||||
<DistanceAllowance v-else :invoice="props.data" />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
invoice: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="travel_allowance">
|
||||
<tr><td colspan="2">
|
||||
Abrechnung einer Reisekostenpauschale
|
||||
</td></tr>
|
||||
<tr>
|
||||
<th>Reiseroute</th>
|
||||
<td>{{props.invoice.travelRoute}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Gesamte Wegstrecke</th>
|
||||
<td>{{props.invoice.distance}} km</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Kilometerpauschale</th>
|
||||
<td>{{props.invoice.distanceAllowance}} Euro / km</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Gesamtbetrag</th>
|
||||
<td style="font-weight: bold">{{props.invoice.amount}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Marterialtransport</th>
|
||||
<td>{{props.invoice.transportation}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Hat Personen mitgenommen</th>
|
||||
<td>{{props.invoice.passengers}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.travel_allowance {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.travel_allowance tr th {
|
||||
width: 300px !important;
|
||||
border-left: 1px solid #ccc;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.travel_allowance tr td,
|
||||
.travel_allowance tr th {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.8em;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.travel_allowance tr td:last-child {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.travel_allowance tr:first-child td:first-child {
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to bottom, #fff, #f6f7f7);
|
||||
border-top: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc !important;
|
||||
}
|
||||
</style>
|
||||
104
app/Domains/Invoice/Views/Partials/myInvoiceDetails/Header.vue
Normal file
104
app/Domains/Invoice/Views/Partials/myInvoiceDetails/Header.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
modeShow: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(["accept", "deny", "fix", "reopen"])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span id="invoice_details_header">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name:</td>
|
||||
<td v-if="modeShow">{{props.data.contactName}}</td>
|
||||
<td v-else style="width: 300px;">{{props.data.contactName}}</td>
|
||||
|
||||
<td v-if="modeShow" style="width: 250px;">Kostenstelle</td>
|
||||
<td v-else style="width: 300px;">Kostensatelle (ursprünglich)</td>
|
||||
|
||||
<td>{{props.data.costUnitName}}</td>
|
||||
<td
|
||||
rowspan="4"
|
||||
style="vertical-align: top;
|
||||
padding-top: 15px;
|
||||
padding-right: 25px;
|
||||
text-align: right;"
|
||||
>
|
||||
<input type="button"
|
||||
v-if="props.data.status === 'new' && modeShow"
|
||||
@click="emit('reject')"
|
||||
class="deny-button"
|
||||
value="Abrechnung zurückziehen" />
|
||||
|
||||
<input type="button"
|
||||
v-if="props.data.status === 'denied' && modeShow"
|
||||
@click="emit('delete')"
|
||||
class="deny-button"
|
||||
value="Abrechnung Endgültig löschen" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Rest der Tabelle bleibt unverändert -->
|
||||
<tr>
|
||||
<td>E-Mail:</td>
|
||||
<td>{{props.data.contactEmail}}</td>
|
||||
<td>
|
||||
Abrechnungsnummer
|
||||
<label v-if="!modeShow"> (ursprünglich)</label>:
|
||||
</td>
|
||||
|
||||
<td>{{props.data.invoiceNumber}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Telefon:</td>
|
||||
<td>{{props.data.contactPhone}}</td>
|
||||
<td>
|
||||
Abrechnungstyp
|
||||
<label v-if="!modeShow"> (Ursprünglich)</label>:
|
||||
</td>
|
||||
<td>{{props.data.invoiceType}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Kontoinhaber*in:</td>
|
||||
<td>{{props.data.accountOwner}}</td>
|
||||
<td>Gesamtbetrag
|
||||
<label v-if="!modeShow"> (Ursprünglich)</label>:
|
||||
</td>
|
||||
<td><strong>{{props.data.amount}}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IBAN:</td>
|
||||
<td>{{props.data.accountIban}}</td>
|
||||
<td>Buchungsinformationen:</td>
|
||||
<td v-if="props.data.donation">Als Spende gebucht</td>
|
||||
<td v-else-if="props.data.alreadyPaid">Beleg ohne Auszahlung</td>
|
||||
<td v-else>Klassische Auszahlung</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status:</td>
|
||||
<td>{{props.data.readableStatus}}</td>
|
||||
<td>Anmerkungen:</td>
|
||||
<td>
|
||||
<span v-if="props.data.status === 'denied'">
|
||||
{{props.data.deniedReason}}
|
||||
</span>
|
||||
<span v-else>{{props.data.comment}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,264 @@
|
||||
<script setup>
|
||||
|
||||
import FullScreenModal from "../../../../../Views/Components/FullScreenModal.vue";
|
||||
import {ref} from "vue";
|
||||
import Modal from "../../../../../Views/Components/Modal.vue";
|
||||
import { useAjax } from "../../../../../../resources/js/components/ajaxHandler.js";
|
||||
import ShowInvoicePartial from "./ShowInvoice.vue";
|
||||
import Header from "./Header.vue";
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}, showInvoice: Boolean
|
||||
})
|
||||
const showInvoice = ref(props.showInvoice)
|
||||
const emit = defineEmits(["close"])
|
||||
const denyInvoiceDialog = ref(false)
|
||||
const { data, loading, error, request } = useAjax()
|
||||
const modeShow = ref(true)
|
||||
const costUnits = ref(null)
|
||||
const newInvoice = ref(null)
|
||||
|
||||
async function acceptInvoice() {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/approved", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (data.status === 'success') {
|
||||
toast.success('Abrechnung wurde freigegeben.');
|
||||
} else {
|
||||
toast.error('Bei der Bearbeitung ist ein Fehler aufgetreten.');
|
||||
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
function close() {
|
||||
emit('reload')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function updateInvoice(formData) {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/update", {
|
||||
method: "POST",
|
||||
body: {
|
||||
invoiceData: formData
|
||||
}
|
||||
});
|
||||
|
||||
if (!data.do_copy) {
|
||||
modeShow.value = true;
|
||||
toast.success('Die Koreektur der Abrechnung wurde gespeichert.');
|
||||
close();
|
||||
} else {
|
||||
modeShow.value = true;
|
||||
newInvoice.value = data.invoice;
|
||||
props.data.id = data.invoice.id;
|
||||
reloadInvoiceFixDialog()
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadInvoiceFixDialog() {
|
||||
const data = await request("api/v1/invoice/details/" + props.data.id, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
newInvoice.value = data.invoice;
|
||||
props.data.id = data.invoice.id;
|
||||
costUnits.value = data.costUnits;
|
||||
props.data.id = data.invoice.id;
|
||||
|
||||
modeShow.value = false;
|
||||
toast.success('Die Abrechnung wurde gespeichert und eine neue Abrechnung wurde erstellt.');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function openInvoiceFixDialog() {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/copy", {
|
||||
method: "POST",
|
||||
body: {
|
||||
}
|
||||
});
|
||||
|
||||
if (data.status === 'success') {
|
||||
costUnits.value = data.costUnits;
|
||||
newInvoice.value = data.invoice;
|
||||
props.data.id = data.invoice.id;
|
||||
modeShow.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function openDenyInvoiceDialog() {
|
||||
denyInvoiceDialog.value = true;
|
||||
}
|
||||
|
||||
async function rejectInvoice() {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/denied", {
|
||||
method: "POST",
|
||||
body: {
|
||||
reason: 'Von Antragssteller*in zurückgezogen'
|
||||
}
|
||||
});
|
||||
|
||||
if (data.status === 'success') {
|
||||
toast.success('Abrechnung wurde zurückgezogen.');
|
||||
} else {
|
||||
toast.error('Bei der Bearbeitung ist ein Fehler aufgetreten.');
|
||||
|
||||
}
|
||||
|
||||
denyInvoiceDialog.value = false;
|
||||
close();
|
||||
}
|
||||
|
||||
async function deleteInvoice() {
|
||||
const data = await request("/api/v1/invoice/details/" + props.data.id + "/change-state/deleted", {
|
||||
method: "POST",
|
||||
|
||||
});
|
||||
|
||||
if (data.status === 'success') {
|
||||
toast.success('Die Abrechnung wurde gelöscht.');
|
||||
} else {
|
||||
toast.error('Beim Bearbeiten ist ein Fehler aufgetreten.');
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<FullScreenModal
|
||||
:show="showInvoice"
|
||||
title="Abrechnungsdetails"
|
||||
@close="emit('close')"
|
||||
>
|
||||
|
||||
<Header :data="props.data"
|
||||
@reject="rejectInvoice"
|
||||
@delete="deleteInvoice"
|
||||
:modeShow="modeShow"
|
||||
/>
|
||||
|
||||
<ShowInvoicePartial :data="props.data" />
|
||||
|
||||
</FullScreenModal>
|
||||
|
||||
<Modal title="Abrechnung ablehnen" :show="denyInvoiceDialog" @close="denyInvoiceDialog = false" >
|
||||
Begründung:
|
||||
<textarea class="mareike-textarea" style="width: 100%; height: 100px; margin-top: 10px;" id="deny_invoice_reason" />
|
||||
<input type="button" class="mareike-button mareike-deny-invoice-button" value="Abrechnung ablehnen" @click="denyInvoice" />
|
||||
</Modal>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.mareike-deny-invoice-button {
|
||||
width: 150px !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#invoice_details_header{
|
||||
font-weight: bold;
|
||||
font-size: 12pt;
|
||||
line-height: 1.8em;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
#invoice_details_header table {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 10px !important;
|
||||
width: 98%;
|
||||
border-color: #c0c0c0;
|
||||
box-shadow: 5px 5px 10px #c0c0c0;
|
||||
margin-bottom: 75px;
|
||||
font-weight: normal;
|
||||
}
|
||||
#invoice_details_header table tr td:first-child {
|
||||
padding-right: 50px;
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
#invoice_details_header table tr td:nth-child(2) {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
#invoice_details_header table tr td:nth-child(3) {
|
||||
padding-right: 50px;
|
||||
width: 100px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#invoice_details_body {
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#invoice_details_body table tr:nth-child(1) td,
|
||||
#invoice_details_body table tr:nth-child(2) td,
|
||||
#invoice_details_body table tr:nth-child(3) td,
|
||||
#invoice_details_body table tr:nth-child(4) td,
|
||||
#invoice_details_body table tr:nth-child(6) td{
|
||||
vertical-align: top;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#invoice_details_body table tr:nth-child(5) td {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
#invoice_details_body table {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
#invoice_details_body table tr:nth-child(1) td:first-child {
|
||||
padding-right: 50px;
|
||||
|
||||
}
|
||||
|
||||
#invoice_details_body table tr:nth-child(1) td:nth-child(2),
|
||||
#invoice_details_body table tr:nth-child(1) td:nth-child(3)
|
||||
{
|
||||
width: 250px;
|
||||
|
||||
}
|
||||
|
||||
.mareike-accept-button {
|
||||
background-color: #36c054 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.mareike-deny-button {
|
||||
background-color: #ee4b5c !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.mareike-fix-button {
|
||||
background-color: #d3d669 !important;
|
||||
color: #67683c !important;
|
||||
}
|
||||
|
||||
.mareike-button {
|
||||
padding: 5px 25px !important;
|
||||
font-size: 11pt !important;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import Icon from "../../../../../Views/Components/Icon.vue";
|
||||
import InvoiceDetails from "./InvoiceDetails.vue";
|
||||
import { useAjax } from "../../../../../../resources/js/components/ajaxHandler.js";
|
||||
import {ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: Object
|
||||
})
|
||||
|
||||
const { request } = useAjax()
|
||||
const invoice = ref(null)
|
||||
const show_invoice = ref(false)
|
||||
const localData = ref(props.data)
|
||||
|
||||
async function openInvoiceDetails(invoiceId) {
|
||||
const url = '/api/v1/invoice/details/' + invoiceId
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'GET' })
|
||||
|
||||
const result = await response.json()
|
||||
invoice.value = result.invoice
|
||||
show_invoice.value = true
|
||||
} catch (err) {
|
||||
console.error('Error fetching invoices:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
const url = "/api/v1/invoice/my-invoices/" + props.data.endpoint
|
||||
try {
|
||||
const response = await fetch(url, { method: 'GET' })
|
||||
if (!response.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const result = await response.json()
|
||||
localData.value = result
|
||||
} catch (err) {
|
||||
console.error('Error fetching invoices:', err)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table v-if="localData.invoices.length > 0" class="invoice-list-table">
|
||||
<tr>
|
||||
<td colspan="6">{{props.data.title}}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="invoice in localData.invoices" :id="'invoice_' + invoice.id">
|
||||
<td>{{invoice.invoiceNumber}}</td>
|
||||
<td>{{invoice.invoiceType}}</td>
|
||||
<td>
|
||||
{{invoice.amount}}
|
||||
</td>
|
||||
<td style="width: 150px;">
|
||||
<Icon v-if="invoice.donation" name="hand-holding-dollar" style="color: #ffffff; background-color: green" />
|
||||
<Icon v-if="invoice.alreadyPaid" name="comments-dollar" style="color: #ffffff; background-color: green" />
|
||||
</td>
|
||||
<td>
|
||||
{{invoice.contactName}}<br />
|
||||
<label v-if="invoice.contactEmail !== '--'">{{invoice.contactEmail}}<br /></label>
|
||||
<label v-if="invoice.contactPhone !== '--'">{{invoice.contactPhone}}<br /></label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input type="button" value="Abrechnung Anzeigen" @click="openInvoiceDetails(invoice.id)" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p v-else>Es sind keine Abrechnungen in dieser Kategorie vorhanden.</p>
|
||||
|
||||
<InvoiceDetails :data="invoice" :show-invoice="show_invoice" v-if="show_invoice" @close="show_invoice = false; reload()" />
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import PdfViewer from "../../../../../Views/Components/PdfViewer.vue";
|
||||
import DistanceAllowance from "./DistanceAllowance.vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span id="invoice_details_body">
|
||||
<PdfViewer :url="'/api/v1/invoice/showReceipt/' + props.data.id" v-if="props.data.documentFilename !== null" />
|
||||
<DistanceAllowance v-else :invoice="props.data" />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import {checkFilesize} from "../../../../../../resources/js/components/InvoiceUploadChecks.js";
|
||||
import RefundData from "./refund-data.vue";
|
||||
import AmountInput from "../../../../../Views/Components/AmountInput.vue";
|
||||
import {useAjax} from "../../../../../../resources/js/components/ajaxHandler.js";
|
||||
import InfoIcon from '../../../../../Views/Components/InfoIcon.vue'
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
|
||||
|
||||
const data = defineProps({
|
||||
eventId: Number,
|
||||
userName: String,
|
||||
userEmail: String,
|
||||
userTelephone: String,
|
||||
userIban: String,
|
||||
userAccountOwner: String,
|
||||
})
|
||||
|
||||
|
||||
const { request } = useAjax();
|
||||
const amount = ref(0.00);
|
||||
const invoiceType = ref(null);
|
||||
const otherText = ref('');
|
||||
const receipt = ref(null)
|
||||
const finalStep = ref(false)
|
||||
|
||||
const invoiceTypeCollection = reactive({
|
||||
invoiceTypes: {}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/core/retrieve-invoice-types');
|
||||
const data = await response.json();
|
||||
Object.assign(invoiceTypeCollection, data);
|
||||
});
|
||||
|
||||
|
||||
function handleFileChange(event) {
|
||||
if (checkFilesize('receipt')) {
|
||||
receipt.value = event.target.files[0]
|
||||
finalStep.value = true
|
||||
} else {
|
||||
event.target.value = null
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset>
|
||||
<legend><span style="font-weight: bolder;">Wofür hast du den Betrag ausgegeben</span></legend>
|
||||
|
||||
<p v-for="availableInvoiceType in invoiceTypeCollection.invoiceTypes">
|
||||
<input
|
||||
name="invpice_type"
|
||||
type="radio"
|
||||
:value="availableInvoiceType.slug"
|
||||
:id="'invoice_type_' + availableInvoiceType.slug"
|
||||
v-model="invoiceType"
|
||||
>
|
||||
<label :for="'invoice_type_' + availableInvoiceType.slug">{{ availableInvoiceType.name }}</label>
|
||||
<InfoIcon :text="'INFO_INVOICE_TYPE_' + availableInvoiceType.slug" /><br />
|
||||
</p>
|
||||
|
||||
|
||||
<label for="invoice_type_other">
|
||||
<input
|
||||
type="text"
|
||||
class="width-full"
|
||||
name="kostengruppe_sonstiges"
|
||||
placeholder="Sonstige"
|
||||
for="invoice_type_other"
|
||||
v-model="otherText"
|
||||
@focus="invoiceType = 'other'"
|
||||
/>
|
||||
</label>
|
||||
|
||||
</fieldset><br /><br />
|
||||
|
||||
<fieldset>
|
||||
<legend><span style="font-weight: bolder;">Wie hoch ist der Betrag</span></legend>
|
||||
<AmountInput v-model="amount" class="width-small" id="amount" name="amount" /> Euro
|
||||
<info-icon></info-icon><br /><br />
|
||||
|
||||
<input
|
||||
v-if="amount != '' && invoiceType !== null"
|
||||
class="mareike-button"
|
||||
onclick="document.getElementById('receipt').click();"
|
||||
type="button"
|
||||
value="Beleg auswählen und fortfahren" />
|
||||
<input accept="application/pdf" type="file" id="receipt" name="receipt" @change="handleFileChange"
|
||||
style="display: none"/>
|
||||
</fieldset><br />
|
||||
|
||||
|
||||
|
||||
<RefundData
|
||||
v-if="finalStep"
|
||||
:eventId="data.eventId"
|
||||
:invoice-type="invoiceType"
|
||||
:amount="amount"
|
||||
:other-text="otherText"
|
||||
:userName="data.userName"
|
||||
:userEmail="data.userEmail"
|
||||
:userTelephone="data.userTelephone"
|
||||
:userIban="data.userIban"
|
||||
:userAccountOwner="data.userAccountOwner"
|
||||
:receipt="receipt"
|
||||
@close="finalStep = false"
|
||||
/>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
157
app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue
Normal file
157
app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import Modal from "../../../../../Views/Components/Modal.vue";
|
||||
import IbanInput from "../../../../../Views/Components/IbanInput.vue";
|
||||
import {useAjax} from "../../../../../../resources/js/components/ajaxHandler.js";
|
||||
import TextResource from "../../../../../Views/Components/TextResource.vue";
|
||||
import {invoiceCheckContactName} from "../../../../../../resources/js/components/InvoiceUploadChecks.js";
|
||||
import {toast} from "vue3-toastify";
|
||||
|
||||
const { request } = useAjax();
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
eventId: Number,
|
||||
invoiceType: String,
|
||||
amount: [String, Number],
|
||||
otherText: String,
|
||||
receipt: File,
|
||||
userName: String,
|
||||
userEmail: String,
|
||||
userTelephone: String,
|
||||
userAccountOwner: String,
|
||||
userIban: String,
|
||||
havePassengers: Number,
|
||||
materialTransportation: Boolean,
|
||||
})
|
||||
|
||||
console.log(props.receipt)
|
||||
|
||||
const finalStep = ref(true)
|
||||
const userName = ref(props.userName)
|
||||
const userEmail = ref(props.userEmail)
|
||||
const userTelephone = ref(props.userTelephone)
|
||||
const userIban = ref(props.userIban)
|
||||
const userAccountOwner = ref(props.userAccountOwner)
|
||||
const sending = ref(false)
|
||||
const success = ref(false)
|
||||
const decision = ref('')
|
||||
const errorMsg = ref('')
|
||||
const confirmation = ref(null)
|
||||
|
||||
|
||||
async function sendData() {
|
||||
if (!userName.value) return
|
||||
|
||||
sending.value = true
|
||||
errorMsg.value = ''
|
||||
success.value = false
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('name', userName.value)
|
||||
formData.append('email', userEmail.value)
|
||||
formData.append('telephone', userTelephone.value)
|
||||
formData.append('amount', props.amount)
|
||||
formData.append('otherText', props.otherText)
|
||||
formData.append('decision', decision.value)
|
||||
formData.append('accountOwner', userAccountOwner.value)
|
||||
formData.append('accountIban', userIban.value)
|
||||
formData.append('havePassengers', props.havePassengers ? 1 : 0)
|
||||
formData.append('materialTransportation', props.materialTransportation ? 1 : 0)
|
||||
|
||||
if (props.receipt) {
|
||||
formData.append('receipt', props.receipt)
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const response = await request('/api/v1/invoice/new/' + props.eventId + '/' + props.invoiceType, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (response.status === 'success') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(result.message);
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :show="finalStep" title='Bitte gib deine Daten ein' @close="emit('close')">
|
||||
<label>
|
||||
<strong>Dein Name Name (kein Pfadiname):</strong>
|
||||
</label><br />
|
||||
<input
|
||||
type="text"
|
||||
@keyup="invoiceCheckContactName();"
|
||||
id="contact_name"
|
||||
name="contact_name" v-model="userName"
|
||||
style="font-size: 14pt; width: 550px;" /><br /><br />
|
||||
|
||||
<label>
|
||||
<strong>E-Mail-Adresse (Für Rückfragen):</strong>
|
||||
</label><br />
|
||||
<input
|
||||
type="email"
|
||||
name="contact_email"
|
||||
v-model="userEmail"
|
||||
style="font-size: 14pt; width: 550px;" /><br /><br />
|
||||
|
||||
<label>
|
||||
<strong>Telefonnummer (für Rückfragen):</strong>
|
||||
</label><br />
|
||||
<input
|
||||
type="text"
|
||||
id="contact_telephone"
|
||||
name="contact_telephone" v-model="userTelephone"
|
||||
style="font-size: 14pt; width: 550px;" /><br /><br />
|
||||
|
||||
<span id="decision" v-if="userName !== '' && decision === ''">
|
||||
<label><br />
|
||||
<strong>Möchtest du den Betrag spenden?</strong>
|
||||
</label><br />
|
||||
|
||||
<input type="button" style="border-radius: 0; width: 100px;" @click="decision='donation'" value="Ja" />
|
||||
<input type="button" style="border-radius: 0; width: 100px;" @click="decision='payout'" value="Nein" />
|
||||
</span>
|
||||
|
||||
<span id="confirm_donation" v-if="decision === 'donation'">
|
||||
<input type="radio" name="confirmation_radio" value="donation" id="confirmation_radio_donation" v-model="confirmation">
|
||||
<TextResource belongsTo="confirmation_radio_donation" textName="CONFIRMATION_DONATE" />
|
||||
<br /><br />
|
||||
<input type="button" class="mareike-button" v-if="confirmation !== null && !sending" @click="sendData" value="Beleg einreichen" />
|
||||
</span>
|
||||
|
||||
<span id="confirm_payment" v-if="decision === 'payout'">
|
||||
<label>
|
||||
<strong>Konto-Inhaber*in:</strong></label><br />
|
||||
<input type="text" name="account_owner" id="account_owner" v-model="userAccountOwner" style="font-size: 14pt; width: 550px;" /><br /><br />
|
||||
|
||||
<label>
|
||||
<strong>IBAN:</strong>
|
||||
</label><br />
|
||||
<IbanInput id="account_iban" name="account_iban" v-model="userIban" style="font-size: 14pt; width: 550px;" /><br /><br />
|
||||
|
||||
<span v-if="userAccountOwner != '' && userIban && userIban.length === 27"><br />
|
||||
<input type="radio" name="confirmation_radio" value="payment" id="confirmation_radio_payment" v-model="confirmation">
|
||||
<TextResource belongsTo="confirmation_radio_payment" textName="CONFIRMATION_PAYMENT" /><br /><br />
|
||||
<input type="button" v-if="confirmation !== null && !sending" @click="sendData" value="Beleg einreichen" />
|
||||
</span>
|
||||
</span>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* optional styling */
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user