Cost units can be edited
This commit is contained in:
@@ -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->tresurers()->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,29 @@
|
|||||||
|
<?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,
|
||||||
|
|
||||||
|
]);
|
||||||
|
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,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
|
||||||
|
|
||||||
|
class CreateCostUnitResponse {
|
||||||
|
|
||||||
|
}
|
||||||
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.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Domains/CostUnit/Controllers/CreateController.php
Normal file
35
app/Domains/CostUnit/Controllers/CreateController.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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\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('mail_on_new')
|
||||||
|
);
|
||||||
|
|
||||||
|
$createCostUnitCommand = new CreateCostUnitCommand($createCostUnitRequest);
|
||||||
|
$result = $createCostUnitCommand->execute();
|
||||||
|
session()->put('message', 'Die laufende Tätigkeit wurde erfolgreich angelegt.');
|
||||||
|
|
||||||
|
return response()->json([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
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.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/Domains/CostUnit/Controllers/ListController.php
Normal file
50
app/Domains/CostUnit/Controllers/ListController.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/Domains/CostUnit/Routes/api.php
Normal file
49
app/Domains/CostUnit/Routes/api.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\CostUnit\Controllers\ChangeStateController;
|
||||||
|
use App\Domains\CostUnit\Controllers\CreateController;
|
||||||
|
use App\Domains\CostUnit\Controllers\EditController;
|
||||||
|
use App\Domains\CostUnit\Controllers\ListController;
|
||||||
|
use App\Domains\CostUnit\Controllers\TreasurersEditController;
|
||||||
|
use App\Domains\UserManagement\Controllers\EmailVerificationController;
|
||||||
|
use App\Domains\UserManagement\Controllers\RegistrationController;
|
||||||
|
use App\Domains\UserManagement\Controllers\ResetPasswordController;
|
||||||
|
use App\Middleware\IdentifyTenant;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
Route::prefix('api/v1')
|
||||||
|
->group(function () {
|
||||||
|
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||||
|
Route::prefix('cost-unit')->group(function () {
|
||||||
|
Route::middleware(['auth'])->group(function () {
|
||||||
|
Route::post('/create-running-job', [CreateController::class, 'createCostUnitRunningJob']);
|
||||||
|
|
||||||
|
|
||||||
|
Route::prefix('/{costUnitId}') ->group(function () {
|
||||||
|
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::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\Dashboard\Controllers\DashboardController;
|
||||||
|
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\Http\Controllers\TestRenderInertiaProvider;
|
||||||
|
use App\Middleware\IdentifyTenant;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
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('/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>
|
||||||
84
app/Domains/CostUnit/Views/List.vue
Normal file
84
app/Domains/CostUnit/Views/List.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<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>
|
||||||
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>
|
||||||
255
app/Domains/CostUnit/Views/Partials/ListCostUnits.vue
Normal file
255
app/Domains/CostUnit/Views/Partials/ListCostUnits.vue
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
<script setup>
|
||||||
|
import {createApp, ref} from 'vue'
|
||||||
|
/*import {
|
||||||
|
_mareike_download_as_zip,
|
||||||
|
|
||||||
|
_mareike_use_webdav
|
||||||
|
} from "../../../assets/javascripts/library";*/
|
||||||
|
//import LoadingModal from "../../../assets/components/LoadingModal.vue";
|
||||||
|
//import Invoices from '../invoices/index.vue'
|
||||||
|
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
|
||||||
|
import CostUnitDetails from "./CostUnitDetails.vue";
|
||||||
|
import {toast} from "vue3-toastify";
|
||||||
|
import Treasurers from "./Treasurers.vue";
|
||||||
|
|
||||||
|
//import CostUnitDetails from "./CostUnitDetails.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()
|
||||||
|
|
||||||
|
if (props.deep_jump_id > 0) {
|
||||||
|
open_invoice_list(props.deep_jump_id, 'new', props.deep_jump_id_sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open_invoice_list(cost_unit_id, endpoint, invoice_id) {
|
||||||
|
const url = '' // `/wp-json/mareike/invoices/list-${endpoint}?invoice_id=${invoice_id}&cost_unit_id=${cost_unit_id}
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { method: 'GET' })
|
||||||
|
if (!response.ok) throw new Error('Fehler beim Laden')
|
||||||
|
invoices.value = await response.json()
|
||||||
|
current_cost_unit.value = cost_unit_id
|
||||||
|
invoice_id = invoice_id
|
||||||
|
showInvoiceList.value = true
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 export_payouts(cost_unit_id) {
|
||||||
|
showLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (_mareike_download_as_zip()) {
|
||||||
|
const response = await fetch("/wp-json/mareike/costunits/export-payouts", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
mareike_nonce: _mareike_nonce(),
|
||||||
|
costunit: cost_unit_id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Fehler beim Export (ZIP)');
|
||||||
|
const blob = await response.blob();
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = `payouts-${cost_unit_id}.zip`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(downloadUrl);
|
||||||
|
} else {
|
||||||
|
await request("/wp-json/mareike/costunits/export-payouts", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
mareike_nonce: _mareike_nonce(),
|
||||||
|
costunit: cost_unit_id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showLoading.value = false;
|
||||||
|
toast.success('Die Abrechnungen wurden exportiert.');
|
||||||
|
} 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" />
|
||||||
|
<input v-else type="button" value="Abrechnungen einsehen" />
|
||||||
|
<br />
|
||||||
|
<input v-if="!costUnit.archived" type="button" 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" />
|
||||||
|
|
||||||
|
|
||||||
|
<LoadingModal :show="showLoading" v-if="_mareike_use_webdav()" message="Die PDF-Dateien werden asynchron erzeugt, diese sollten in 10 Minuten auf dem Webdav-Server liegen', 'mareike')" />
|
||||||
|
<LoadingModal :show="showLoading" v-else message='Die Abrechnungen werden exportiert, bitte warten.' />
|
||||||
|
</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>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.costunit-list {
|
||||||
|
width: 96% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
99
app/Domains/CostUnit/Views/Partials/Treasurers.vue
Normal file
99
app/Domains/CostUnit/Views/Partials/Treasurers.vue
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<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/retreive-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')"
|
||||||
|
>
|
||||||
|
Zuständige Schatzis:
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -24,7 +24,11 @@ class RegistrationController extends CommonController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$inertiaProvider = new InertiaProvider('UserManagement/Registration', ['errors' => $errors, 'appName' => app('tenant')->name]);
|
$inertiaProvider = new InertiaProvider('UserManagement/Registration', [
|
||||||
|
'errors' => $errors,
|
||||||
|
'appName' => app('tenant')->name,
|
||||||
|
'tenant' => app('tenant'),
|
||||||
|
]);
|
||||||
return $inertiaProvider->render();
|
return $inertiaProvider->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ use App\Domains\UserManagement\Controllers\EmailVerificationController;
|
|||||||
use App\Domains\UserManagement\Controllers\RegistrationController;
|
use App\Domains\UserManagement\Controllers\RegistrationController;
|
||||||
use App\Domains\UserManagement\Controllers\ResetPasswordController;
|
use App\Domains\UserManagement\Controllers\ResetPasswordController;
|
||||||
use App\Middleware\IdentifyTenant;
|
use App\Middleware\IdentifyTenant;
|
||||||
|
use App\Providers\GlobalDataProvider;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
Route::prefix('v1')
|
Route::prefix('v1')
|
||||||
->group(function () {
|
->group(function () {
|
||||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||||
Route::post('/register', [RegistrationController::class, 'doRegistration']);
|
|
||||||
Route::post('/register/confirmEmail', [EmailVerificationController::class, 'doVerification']);
|
|
||||||
Route::post('/reset-password', [ResetPasswordController::class, 'doResetPassword']);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout title='Registrieren' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
|
<AppLayout title='Registrieren'>
|
||||||
<form method="POST" action="/register" @submit.prevent="submit">
|
<form method="POST" action="/register" @submit.prevent="submit">
|
||||||
<input type="hidden" name="_token" :value="csrfToken" />
|
<input type="hidden" name="_token" :value="csrfToken" />
|
||||||
<shadowed-box style="width: 75%; margin: 150px auto; padding: 20px;">
|
<shadowed-box style="width: 75%; margin: 150px auto; padding: 20px;">
|
||||||
|
|||||||
40
app/Models/CostUnit.php
Normal file
40
app/Models/CostUnit.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Scopes\InstancedModel;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $name
|
||||||
|
* @property string $type
|
||||||
|
* @property string $billing_deadline
|
||||||
|
* @property string $distance_allowance
|
||||||
|
* @property boolean $mail_on_new
|
||||||
|
* @property boolean $allow_new
|
||||||
|
* @property boolean $archived
|
||||||
|
* @property Tenant $tenant
|
||||||
|
*/
|
||||||
|
class CostUnit extends InstancedModel
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'tenant',
|
||||||
|
'type',
|
||||||
|
'billing_deadline',
|
||||||
|
'distance_allowance',
|
||||||
|
'mail_on_new',
|
||||||
|
'allow_new',
|
||||||
|
'archived',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function tresurers() : BelongsToMany{
|
||||||
|
return $this->belongsToMany(User::class, 'cost_unit_treasurers', 'cost_unit_id', 'user_id')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetTreasurers() {
|
||||||
|
$this->tresurers()->detach();
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ class User extends Authenticatable
|
|||||||
public function getFullname() : string {
|
public function getFullname() : string {
|
||||||
return sprintf('%1$1s %2$s %3$s',
|
return sprintf('%1$1s %2$s %3$s',
|
||||||
$this->firstname,
|
$this->firstname,
|
||||||
$this->nickname !== '' ? '(' . $this->nickname . ')' : '',
|
$this->nickname !== null ? '(' . $this->nickname . ')' : '',
|
||||||
$this->lastname
|
$this->lastname
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -103,4 +104,14 @@ class User extends Authenticatable
|
|||||||
public function getNicename() : string {
|
public function getNicename() : string {
|
||||||
return $this->nickname ?? $this->firstname;
|
return $this->nickname ?? $this->firstname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function localGroup() : Tenant {
|
||||||
|
return $this->belongsTo(Tenant::class, 'local_group', 'slug')->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function costUnits()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(CostUnit::class, 'cost_unit_treasurers', 'user_id', 'cost_unit_id')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Models\Tenant;
|
use App\Enumerations\UserRole;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Repositories\UserRepository;
|
|
||||||
use App\Resources\UserResource;
|
use App\Resources\UserResource;
|
||||||
|
|
||||||
class GlobalDataProvider {
|
class GlobalDataProvider {
|
||||||
@@ -14,10 +13,10 @@ class GlobalDataProvider {
|
|||||||
$this->user = auth()->user();
|
$this->user = auth()->user();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'user' => null !== $this->user ? new UserResource($this->user)->toArray(request()) : [],
|
'user' => null !== $this->user ? new UserResource($this->user)->toArray(request()) : null,
|
||||||
'navbar' => $this->generateNavbar(),
|
'navbar' => $this->generateNavbar(),
|
||||||
'tenant' => app('tenant'),
|
'tenant' => app('tenant'),
|
||||||
'availableLocalGroups' => Tenant::where(['is_active_local_group' => true])->get(),
|
'activeUsers' => $this->getActiveUsers(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,8 +33,13 @@ class GlobalDataProvider {
|
|||||||
$navigation['personal'][] = ['url' => '/personal-data', 'display' => 'Meine Daten'];
|
$navigation['personal'][] = ['url' => '/personal-data', 'display' => 'Meine Daten'];
|
||||||
$navigation['personal'][] = ['url' => '/messages', 'display' => 'Meine Nachrichten'];
|
$navigation['personal'][] = ['url' => '/messages', 'display' => 'Meine Nachrichten'];
|
||||||
|
|
||||||
|
if (
|
||||||
|
in_array($this->user->user_role_local_group, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER] ) ||
|
||||||
|
$this->user->user_role_main === UserRole::USER_ROLE_ADMIN
|
||||||
|
) {
|
||||||
|
$navigation['costunits'][] = ['url' => '/cost-unit/list', 'display' => 'Kostenstellen'];
|
||||||
$navigation['costunits'][] = ['url' => '/cost-unit/create', 'display' => 'Neue laufende Tätigkeit'];
|
$navigation['costunits'][] = ['url' => '/cost-unit/create', 'display' => 'Neue laufende Tätigkeit'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$navigation['common'][] = ['url' => '/capture-invoice', 'display' => 'Neue Abrechnung'];
|
$navigation['common'][] = ['url' => '/capture-invoice', 'display' => 'Neue Abrechnung'];
|
||||||
@@ -43,4 +47,22 @@ class GlobalDataProvider {
|
|||||||
|
|
||||||
return $navigation;
|
return $navigation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getActiveUsers() : array {
|
||||||
|
$activeUsers = [];
|
||||||
|
$conditions = ['active' => true];
|
||||||
|
|
||||||
|
if (app('tenant')->slug !== 'lv') {
|
||||||
|
$conditions['local_group'] = app('tenant')->slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rawUsers = User::where($conditions)->get() as $user) {
|
||||||
|
$userResource = new UserResource($user);
|
||||||
|
$activeUsers[] = $userResource->getReducedData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $activeUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,16 @@ final class InertiaProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function render() : Response {
|
public function render() : Response {
|
||||||
|
if (null !== session()->get('message')) {
|
||||||
|
$this->props['message'] = session()->get('message');
|
||||||
|
session()->forget('message');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->props['availableLocalGroups'] = Tenant::where(['is_active_local_group' => true])->get();
|
||||||
|
|
||||||
return Inertia::render(
|
return Inertia::render(
|
||||||
str_replace('/', '/Views/', $this->vueFile),
|
str_replace('/', '/Views/', $this->vueFile),
|
||||||
$this->props
|
$this->props
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
app/RelationModels/CostUnitTreasures.php
Normal file
11
app/RelationModels/CostUnitTreasures.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\RelationModels;
|
||||||
|
|
||||||
|
use App\Scopes\CommonModel;
|
||||||
|
|
||||||
|
class CostUnitTreasures extends CommonModel
|
||||||
|
{
|
||||||
|
protected $table = 'cost_unit_treasurers';
|
||||||
|
protected $fillable = ['cost_unit_id', 'user_id'];
|
||||||
|
}
|
||||||
87
app/Repositories/CostUnitRepository.php
Normal file
87
app/Repositories/CostUnitRepository.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Enumerations\CostUnitType;
|
||||||
|
use App\Enumerations\UserRole;
|
||||||
|
use App\Models\CostUnit;
|
||||||
|
use App\Resources\CostUnitResource;
|
||||||
|
|
||||||
|
class CostUnitRepository {
|
||||||
|
public function getCurrentEvents() : array {
|
||||||
|
return $this->getCostUnitsByCriteria([
|
||||||
|
'allow_new' => true,
|
||||||
|
'type' => CostUnitType::COST_UNIT_TYPE_EVENT,
|
||||||
|
'archived' => false
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRunningJobs() : array {
|
||||||
|
return $this->getCostUnitsByCriteria([
|
||||||
|
'allow_new' => true,
|
||||||
|
'type' => CostUnitType::COST_UNIT_TYPE_RUNNING_JOB,
|
||||||
|
'archived' => false
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getArchivedCostUnits() : array {
|
||||||
|
return $this->getCostUnitsByCriteria([
|
||||||
|
'archived' => true,
|
||||||
|
'allow_new' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClosedCostUnits() : array{
|
||||||
|
return $this->getCostUnitsByCriteria([
|
||||||
|
'archived' => false,
|
||||||
|
'allow_new' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById(int $id) : ?CostUnit {
|
||||||
|
$costUnits = self::getCostUnitsByCriteria(['id' => $id], false);
|
||||||
|
if (count($costUnits) === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $costUnits[0];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCostUnitsByCriteria(array $criteria, bool $forDisplay = true) : array {
|
||||||
|
$tenant = app('tenant');
|
||||||
|
|
||||||
|
$canSeeAll = false;
|
||||||
|
$user = Auth()->user();
|
||||||
|
|
||||||
|
if ($tenant->slug !== 'lv') {
|
||||||
|
if (
|
||||||
|
$user->user_role_main === UserRole::USER_ROLE_ADMIN ||
|
||||||
|
in_array( $user->user_role_local_group, [UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_ADMIN])
|
||||||
|
) {
|
||||||
|
$canSeeAll = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
in_array( $user->user_role_main, [UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_ADMIN])
|
||||||
|
) {
|
||||||
|
$canSeeAll = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$visibleCostUnits = [];
|
||||||
|
/** @var CostUnit $costUnit */
|
||||||
|
foreach (Costunit::where($criteria)->get() as $costUnit) {
|
||||||
|
if ($costUnit->tresurers()->where('user_id', $user->id)->exists() || $canSeeAll) {
|
||||||
|
if ($forDisplay) {
|
||||||
|
$visibleCostUnits[] = new CostUnitResource($costUnit)->toArray(request());
|
||||||
|
} else {
|
||||||
|
$visibleCostUnits[] = $costUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $visibleCostUnits;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Resources/CostUnitResource.php
Normal file
43
app/Resources/CostUnitResource.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Resources;
|
||||||
|
|
||||||
|
use App\Models\CostUnit;
|
||||||
|
use App\ValueObjects\Amount;
|
||||||
|
|
||||||
|
class CostUnitResource {
|
||||||
|
private CostUnit $costUnit;
|
||||||
|
|
||||||
|
public function __construct(CostUnit $costUnit) {
|
||||||
|
$this->costUnit = $costUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray($request) {
|
||||||
|
$totalAmount = 0;
|
||||||
|
$donatedAmount = 0;
|
||||||
|
|
||||||
|
$countNewInvoices = 0;
|
||||||
|
$countApprovedInvoices = 0;
|
||||||
|
$countDonatedInvoices = 0;
|
||||||
|
$countDeniedInvoices = 0;
|
||||||
|
|
||||||
|
|
||||||
|
$data = array_merge(
|
||||||
|
$this->costUnit->toArray(),
|
||||||
|
[
|
||||||
|
'distanceAllowanceSmall' => new Amount($this->costUnit->distance_allowance, '')->toString(),
|
||||||
|
'distanceAllowanceFull' => new Amount($this->costUnit->distance_allowance, ' Euro')->toString(),
|
||||||
|
'totalAmount' => new Amount($totalAmount, ' Euro')->toString(),
|
||||||
|
'donatedAmount' => new Amount($donatedAmount, ' Euro')->toString(),
|
||||||
|
'countNewInvoices' => $countNewInvoices,
|
||||||
|
'countApprovedInvoices' => $countApprovedInvoices,
|
||||||
|
'countDonatedInvoices' => $countDonatedInvoices,
|
||||||
|
'countDeniedInvoices' => $countDeniedInvoices,
|
||||||
|
'treasurers' => $this->costUnit->tresurers()->get()->map(fn($user) => new UserResource($user))->toArray(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -29,6 +29,16 @@ class UserResource extends JsonResource {
|
|||||||
unset($data['activation_token_expires_at']);
|
unset($data['activation_token_expires_at']);
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReducedData(): array {
|
||||||
|
return [
|
||||||
|
'id' => $this->user->id,
|
||||||
|
'nicename' => $this->user->getNicename(),
|
||||||
|
'fullname' => $this->user->getFullName(),
|
||||||
|
'firstname' => $this->user->firstname,
|
||||||
|
'lastname' => $this->user->lastname,
|
||||||
|
'localGroup' => $this->user->localGroup()->name,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
namespace App\Scopes;
|
namespace App\Scopes;
|
||||||
|
|
||||||
use App\Providers\AuthCheckProvider;
|
use App\Providers\AuthCheckProvider;
|
||||||
|
use App\Repositories\CostUnitRepository;
|
||||||
use App\Repositories\UserRepository;
|
use App\Repositories\UserRepository;
|
||||||
|
|
||||||
abstract class CommonController {
|
abstract class CommonController {
|
||||||
protected UserRepository $users;
|
protected UserRepository $users;
|
||||||
|
protected CostUnitRepository $costUnits;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->users = new UserRepository();
|
$this->users = new UserRepository();
|
||||||
|
$this->costUnits = new CostUnitRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function checkAuth() {
|
protected function checkAuth() {
|
||||||
|
|||||||
35
app/ValueObjects/Amount.php
Normal file
35
app/ValueObjects/Amount.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\ValueObjects;
|
||||||
|
|
||||||
|
class Amount {
|
||||||
|
private string $currency;
|
||||||
|
private float $amount;
|
||||||
|
|
||||||
|
public static function fromString(string $amountString, string $currency = ' Euro') : Amount {
|
||||||
|
return new self(
|
||||||
|
(float)str_replace(',', '.', $amountString)
|
||||||
|
, $currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(float $amount, string $currency) {
|
||||||
|
$this->currency = $currency;
|
||||||
|
$this->amount = $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAmount() : float {
|
||||||
|
return $this->amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrency() : string {
|
||||||
|
return $this->currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toString() : string {
|
||||||
|
$value = number_format( round( $this->amount, 2 ), 2, ',', '.' );
|
||||||
|
return sprintf('%1$s %2$s', $value, $this->currency)
|
||||||
|
|> trim(...)
|
||||||
|
|> function (string $value) : string { return str_replace('.', ',', $value); };
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Views/Components/AmountInput.vue
Normal file
46
app/Views/Components/AmountInput.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup>
|
||||||
|
const model = defineModel({ type: String, default: '' })
|
||||||
|
|
||||||
|
function onInput(e) {
|
||||||
|
let val = e.target.value
|
||||||
|
|
||||||
|
// nur Ziffern und Komma erlauben
|
||||||
|
val = val.replace(/[^0-9,]/g, '')
|
||||||
|
|
||||||
|
// nur das erste Komma behalten
|
||||||
|
const parts = val.split(',')
|
||||||
|
if (parts.length > 2) {
|
||||||
|
val = parts[0] + ',' + parts.slice(1).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nachkommastellen auf 2 begrenzen
|
||||||
|
if (parts.length === 2) {
|
||||||
|
val = parts[0] + ',' + parts[1].slice(0, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
model.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeypress(e) {
|
||||||
|
const key = e.key
|
||||||
|
|
||||||
|
// Ziffern sind immer erlaubt
|
||||||
|
if (/[0-9]/.test(key)) return
|
||||||
|
|
||||||
|
// Komma nur wenn noch keines im Wert enthalten ist
|
||||||
|
if (key === ',' && !model.value.includes(',')) return
|
||||||
|
|
||||||
|
// alles andere blocken (inkl. Minuszeichen, Punkte, Buchstaben)
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="model"
|
||||||
|
inputmode="decimal"
|
||||||
|
@input="onInput"
|
||||||
|
@keypress="onKeypress"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
139
app/Views/Components/Modal.vue
Normal file
139
app/Views/Components/Modal.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="modal-overlay"
|
||||||
|
@click.self="close"
|
||||||
|
>
|
||||||
|
<transition name="scale">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
ref="modalRef"
|
||||||
|
class="modal-content"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="title"><label>{{ title }}</label>
|
||||||
|
<span @click="close" class="dashicons dashicons-dismiss modal-close"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean,
|
||||||
|
title: { type: String, default: 'Modal' }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const modalRef = ref(null)
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. ESC-Key schließt Modal
|
||||||
|
*/
|
||||||
|
function handleKeyDown(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Focus-Trap
|
||||||
|
if (e.key === 'Tab' && modalRef.value) {
|
||||||
|
const focusable = modalRef.value.querySelectorAll(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
)
|
||||||
|
if (focusable.length === 0) return
|
||||||
|
|
||||||
|
const first = focusable[0]
|
||||||
|
const last = focusable[focusable.length - 1]
|
||||||
|
|
||||||
|
if (e.shiftKey && document.activeElement === first) {
|
||||||
|
e.preventDefault()
|
||||||
|
last.focus()
|
||||||
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||||||
|
e.preventDefault()
|
||||||
|
first.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. Body-Scroll sperren wenn Modal offen
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
async (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
await nextTick()
|
||||||
|
modalRef.value?.focus() // Setzt Focus ins Modal
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
document.body.style.overflow = '' // fallback cleanup
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.scale-enter-active,
|
||||||
|
.scale-leave-active {
|
||||||
|
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
.scale-enter-from,
|
||||||
|
.scale-leave-to {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
style: { type: String},
|
style: { type: String},
|
||||||
class: { type: String},
|
class: { type: String},
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(props.style)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
115
app/Views/Components/TabbedPage.vue
Normal file
115
app/Views/Components/TabbedPage.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, shallowRef, onMounted } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabs: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
// [{ title: "Titel", component: Component, endpoint: "/wp-json/..." }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeTab = ref(null) // aktuell ausgewählter Tab
|
||||||
|
const tabData = ref({}) // Daten für jeden Tab
|
||||||
|
const tabComponent = shallowRef(null) // Komponente für aktuellen Tab
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
async function selectTab(index) {
|
||||||
|
const tab = props.tabs[index]
|
||||||
|
activeTab.value = index
|
||||||
|
tabComponent.value = null
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csrfToken = document
|
||||||
|
.querySelector('meta[name="csrf-token"]')
|
||||||
|
?.getAttribute('content')
|
||||||
|
|
||||||
|
const response = await fetch(tab.endpoint, {
|
||||||
|
method: 'GET', // oder POST, PUT, DELETE …
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': csrfToken,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
credentials: 'same-origin', // wichtig für Session/Auth
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Fehler: " + response.status)
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
tabData.value[index] = json
|
||||||
|
tabComponent.value = tab.component
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ersten Tab automatisch laden
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.tabs.length > 0) {
|
||||||
|
selectTab(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tabs">
|
||||||
|
<!-- Tab Header -->
|
||||||
|
<div class="tab-header">
|
||||||
|
<button
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="index"
|
||||||
|
:class="['tab-button', { active: activeTab === index }]"
|
||||||
|
@click="selectTab(index)"
|
||||||
|
>
|
||||||
|
{{ tab.title }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<div class="tab-content" id="tab-content">
|
||||||
|
<div v-if="loading">⏳ Lädt...</div>
|
||||||
|
<div v-else-if="error">❌ {{ error }}</div>
|
||||||
|
<component
|
||||||
|
v-else-if="tabComponent"
|
||||||
|
:is="tabComponent"
|
||||||
|
:data="tabData[activeTab]"
|
||||||
|
:deep_jump_id="tabs[activeTab].deep_jump_id"
|
||||||
|
:deep_jump_id_sub="tabs[activeTab].deep_jump_id_sub"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tabs {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.tab-header {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.tab-button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
.tab-button.active {
|
||||||
|
font-weight: bold;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 2px solid #0073aa;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,28 +15,32 @@ return new class extends Migration {
|
|||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Schema::create('cost_units', function (Blueprint $table) {
|
Schema::create('cost_units', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('tenant');
|
$table->string('tenant');
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
$table->string('type');
|
$table->string('type');
|
||||||
$table->dateTime('billing_deadline');
|
$table->dateTime('billing_deadline')->nullable();
|
||||||
$table->float('distance_allowance');
|
$table->float('distance_allowance');
|
||||||
$table->boolean('mail_on_new')->default(true);
|
$table->boolean('mail_on_new')->default(true);
|
||||||
$table->boolean('allow_new')->default(true);
|
$table->boolean('allow_new')->default(true);
|
||||||
$table->boolean('archived')->default(false);
|
$table->boolean('archived')->default(false);
|
||||||
$table->string('treasurers');
|
|
||||||
|
|
||||||
$table->foreign('tenant')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate();
|
$table->foreign('tenant')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
$table->foreign('type')->references('slug')->on('cost_unit_types')->cascadeOnDelete()->cascadeOnUpdate();
|
$table->foreign('type')->references('slug')->on('cost_unit_types')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Schema::create('cost_unit_treasurers', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('cost_unit_id')->constrained('cost_units', 'id')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
$table->foreignId('user_id')->constrained('users', 'id')->cascadeOnDelete()->cascadeOnUpdate();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
|
Schema::dropIfExists('cost_unit_treasurers');
|
||||||
Schema::dropIfExists('cost_units');
|
Schema::dropIfExists('cost_units');
|
||||||
Schema::dropIfExists('cost_unit_types');
|
Schema::dropIfExists('cost_unit_types');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ html {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -105,14 +106,22 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
height: 40px;
|
|
||||||
background: #666666;
|
background: #666666;
|
||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid #ddd;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
z-index: 100;
|
||||||
|
line-height: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer table td:first-child {
|
||||||
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@@ -132,4 +141,5 @@ th:after {
|
|||||||
display: block;
|
display: block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
40
public/css/costunits.css
Normal file
40
public/css/costunits.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
.costunit-list table tr th {
|
||||||
|
font-weight: normal;
|
||||||
|
width: 150px !important;
|
||||||
|
padding-right: 20px;
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.8em;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.costunit-list table tr td {
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.8em;
|
||||||
|
}
|
||||||
|
.costunit-list table tr td:not(:last-child) {
|
||||||
|
font-weight: normal;
|
||||||
|
width: 150px;
|
||||||
|
padding-right: 20px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.costunit-list table tr td input[type="button"] {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.costunit-list table
|
||||||
|
{
|
||||||
|
width: calc(100% - 50px) !important;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #cccccc;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 5px 5px 10px #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.costunit-list table thead td {
|
||||||
|
font-weight: bolder;
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
19
public/css/dimensions.css
Normal file
19
public/css/dimensions.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.width-small {
|
||||||
|
width: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-medium {
|
||||||
|
width: 250px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-half-full {
|
||||||
|
width: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-20 {
|
||||||
|
padding-left: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-20 {
|
||||||
|
padding-right: 20px !important;
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
|
input[type="date"],
|
||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 13pt;
|
font-size: 13pt;
|
||||||
@@ -30,6 +31,7 @@ select {
|
|||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
input[type="email"]:focus,
|
input[type="email"]:focus,
|
||||||
input[type="password"]:focus,
|
input[type="password"]:focus,
|
||||||
|
input[type="date"]:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #1d4899;
|
border-color: #1d4899;
|
||||||
@@ -60,3 +62,9 @@ input[type="submit"]:hover {
|
|||||||
color: red;
|
color: red;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #47a0d8;
|
||||||
|
}
|
||||||
|
|||||||
67
public/css/modalBox.css
Normal file
67
public/css/modalBox.css
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
.modal {
|
||||||
|
border-radius: 8px;
|
||||||
|
position: relative;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||||
|
font-family: sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
width: 90%;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
padding: 0 !important;
|
||||||
|
font-family: sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: relative;
|
||||||
|
top: -12px;
|
||||||
|
right: -20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 7px;
|
||||||
|
background-color: #3272C0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background-color: #3272C0;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
font-family: sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .title {
|
||||||
|
font-size: 15pt;
|
||||||
|
padding-top: 5px !important;
|
||||||
|
color: #ffffff;
|
||||||
|
padding-left: 20px;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .title label {
|
||||||
|
width: 90% !important;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .title button {
|
||||||
|
top: -30px !important;
|
||||||
|
position: relative;
|
||||||
|
right: -40px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-style: none;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ export function useAjax() {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
const data = ref(null)
|
const data = ref(null)
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||||
|
|
||||||
async function request(url, options = {}) {
|
async function request(url, options = {}) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -15,6 +16,7 @@ export function useAjax() {
|
|||||||
method: options.method || "GET",
|
method: options.method || "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
'X-CSRF-TOKEN': csrfToken,
|
||||||
...(options.headers || {}),
|
...(options.headers || {}),
|
||||||
},
|
},
|
||||||
body: options.body ? JSON.stringify(options.body) : null,
|
body: options.body ? JSON.stringify(options.body) : null,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const globalProps = reactive({
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const response = await fetch('/api/v1/retreive-global-data');
|
const response = await fetch('/api/v1/retreive-global-data');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
Object.assign(globalProps, data); // reactive wird automatisch aktualisiert
|
Object.assign(globalProps, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -131,7 +131,23 @@ const props = defineProps({
|
|||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
© 2026 Your Company
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Version 4.0.0</td>
|
||||||
|
<td>
|
||||||
|
mareike - Mdodernes Anmeldesystem und richti einfache Kostenerfassung
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Impressum
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Datenschutzerklärung
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
© 2022 - 2026
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="/css/app.css" />
|
<link rel="stylesheet" href="/css/app.css" />
|
||||||
|
<link rel="stylesheet" href="/css/dimensions.css" />
|
||||||
<link rel="stylesheet" href="/css/elements.css" />
|
<link rel="stylesheet" href="/css/elements.css" />
|
||||||
|
<link rel="stylesheet" href="/css/modalBox.css" />
|
||||||
|
<link rel="stylesheet" href="/css/costunits.css" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
require __DIR__.'/../app/Domains/UserManagement/Routes/api.php';
|
require __DIR__.'/../app/Domains/UserManagement/Routes/api.php';
|
||||||
|
|
||||||
|
|
||||||
|
use App\Domains\UserManagement\Controllers\EmailVerificationController;
|
||||||
|
use App\Domains\UserManagement\Controllers\RegistrationController;
|
||||||
|
use App\Domains\UserManagement\Controllers\ResetPasswordController;
|
||||||
|
use App\Middleware\IdentifyTenant;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
Route::prefix('v1')
|
||||||
|
->group(function () {
|
||||||
|
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||||
|
Route::post('/register', [RegistrationController::class, 'doRegistration']);
|
||||||
|
Route::post('/register/confirmEmail', [EmailVerificationController::class, 'doVerification']);
|
||||||
|
Route::post('/reset-password', [ResetPasswordController::class, 'doResetPassword']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Route;
|
|||||||
|
|
||||||
require_once __DIR__ . '/../app/Domains/UserManagement/Routes/web.php';
|
require_once __DIR__ . '/../app/Domains/UserManagement/Routes/web.php';
|
||||||
require_once __DIR__ . '/../app/Domains/CostUnit/Routes/web.php';
|
require_once __DIR__ . '/../app/Domains/CostUnit/Routes/web.php';
|
||||||
|
require_once __DIR__ . '/../app/Domains/CostUnit/Routes/api.php';
|
||||||
|
|
||||||
|
|
||||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user