Compare commits

...

13 Commits

Author SHA1 Message Date
ab711109a7 Pdf Viewer 2026-02-13 10:54:17 +01:00
72623df38f Pdf Viewer 2026-02-13 10:52:40 +01:00
9fd6839878 Small fixes 2026-02-13 10:41:20 +01:00
fd403f8520 Operation processes on invoices 2026-02-13 00:11:51 +01:00
882752472e Invoice Widgets completed 2026-02-11 19:38:06 +01:00
87531237c7 Show own invoices 2026-02-11 15:44:43 +01:00
ee7fc637f1 Invoices can be uploaded 2026-02-11 15:40:06 +01:00
bccfc11687 Cost units can be edited 2026-02-08 20:06:38 +01:00
6fc65e195c API-Route to new global variables 2026-02-05 09:18:24 +01:00
e9ae850002 Display nicename in dashboard 2026-02-05 07:40:00 +01:00
11108bdfcc Basic user management 2026-02-05 00:46:22 +01:00
e280fcfba8 Basic design created 2026-02-03 09:33:18 +01:00
3570f442f5 Basic tenant structure 2026-01-31 20:07:41 +01:00
184 changed files with 9038 additions and 190 deletions

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@
Homestead.json
Homestead.yaml
Thumbs.db
/docker-compose.yaml

View File

@@ -2,6 +2,11 @@
FRONTEND_DIR ?= .
setup:
rm -f docker-compose.yaml
cp docker-compose.dev docker-compose.yaml
docker-compose up -d
frontend:
@cd $(FRONTEND_DIR) && \
export QT_QPA_PLATFORM=offscreen && \

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
class ChangeCostUnitDetailsResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
class ChangeCostUnitStateResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
class ChangeCostUnitTreasurersResponse {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Domains\CostUnit\Actions\CreateCostUnit;
class CreateCostUnitResponse {
}

View 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.'
]);
}
}

View 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([]);
}
}

View File

@@ -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
]);
}
}

View 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.',
]);
}
}

View 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(),
]);
}
}

View File

@@ -0,0 +1,28 @@
<?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) {
$inertiaProvider = new InertiaProvider('CostUnit/Open', [
'costUnitId' => $costUnitId
]);
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,
]);
}
}

View File

@@ -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.'
]);
}
}

View File

@@ -0,0 +1,53 @@
<?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\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::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']);
});
});
});
});
});

View 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']);
});

View 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>

View 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>

View 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>

View 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({
costUnitId: Number
})
const urlParams = new URLSearchParams(window.location.search)
const initialCostUnitId = props.cost_unit_id
const initialInvoiceId = props.invoice_id
const tabs = [
{
title: 'Neue Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/new",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Nichtexportierte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/approved",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Exportierte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/invoice-list/exported",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Abgelehnte Abrechnungen',
component: ListInvoices,
endpoint: "/api/v1/cost-unit/" + props.costUnitId + "/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">
<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>

View 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>

View File

@@ -0,0 +1,246 @@
<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);
}
}
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 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" @click="loadInvoices(costUnit.id)" />
<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> &nbsp;
<strong @click="editTreasurers(costUnit.id)" v-if="!costUnit.archived" class="link">Schatzis zuweisen</strong> &nbsp;
<strong @click="denyNewRequests(costUnit.id)" v-if="costUnit.allow_new" class="link" style="color: #ff0000">Neue Abrechnungen verbieten</strong> &nbsp;
<strong @click="allowNewRequests(costUnit.id)" v-if="!costUnit.allow_new && !costUnit.archived" class="link" style="color: #529a30">Neue Abrechnungen erlauben</strong> &nbsp;
<strong @click="archiveCostUnit(costUnit.id)" v-if="!costUnit.allow_new && !costUnit.archived" class="link" style="color: #ff0000">Veranstaltung archivieren</strong> &nbsp;
</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>

View 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>

View 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>

View File

@@ -0,0 +1,40 @@
<?php
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) {
if ($this->checkAuth()) {
return $this->renderForLoggedInUser($request);
}
return redirect()->intended('/login');
}
private function renderForLoggedInUser(Request $request) {
$authCheckProvider = new AuthCheckProvider;
$inertiaProvider = new InertiaProvider('Dashboard/Dashboard', ['appName' => app('tenant')->name]);
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]);
}
}

View 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']);
});
});
});

View File

@@ -0,0 +1 @@
<?php

View File

@@ -0,0 +1,37 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import {onMounted} from "vue";
import {toast} from "vue3-toastify";
</script>
<template>
<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
</shadowed-box>
</diV>
</AppLayout>
</template>
<style>
.dashboard-widget-container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
position: relative;
}
.dashboard-widget-box {
flex-grow: 1; display: inline-block;
height: 150px;
margin: 0 10px;
}
</style>

View 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="'/invoices/my-invoices/' + invoice.slug" class="link">{{invoice.title}} ({{invoice.count}})</a>
<label>
{{invoice.amount}}
</label>
</p>
</template>
<style scoped>
</style>

View File

@@ -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>

View File

@@ -0,0 +1,51 @@
<?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;
}
if ($this->request->invoice->save()) {
$response->success = true;
}
return $response;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Invoice\Actions\ChangeStatus;
class ChangeStatusResponse {
public bool $success;
function __construct() {
$this->success = false;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,33 @@
<?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;
class UpdateInvoiceCommand {
private UpdateInvoiceRequest $request;
public function __construct(UpdateInvoiceRequest $request) {
$this->request = $request;
}
public function execute() : UpdateInvoiceResponse {
$response = new UpdateInvoiceResponse();
$this->request->invoice->amount = $this->request->amount->getAmount();
$this->request->invoice->cost_unit_id = $this->request->costUnit->id;
$this->request->invoice->type = $this->request->invoiceType;
$this->request->invoice->comment = $this->request->comment;
$this->request->invoice->save();
$request = new ChangeStatusRequest($this->request->invoice, InvoiceStatus::INVOICE_STATUS_APPROVED);
$changeStatusCommand = new ChangeStatusCommand($request);
$changeStatusCommand->execute();
return $response;
}
}

View File

@@ -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 string $invoiceType;
public CostUnit $costUnit;
public Invoice $invoice;
public Amount $amount;
public function __construct(Invoice $invoice, string $comment, string $invoiceType, CostUnit $costUnit, Amount $amount) {
$this->comment = $comment;
$this->invoiceType = $invoiceType;
$this->costUnit = $costUnit;
$this->invoice = $invoice;
$this->amount = $amount;
}
}

View File

@@ -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;
}
}

View 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([]);
}
}

View File

@@ -0,0 +1,132 @@
<?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\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);
$updateInvoiceRequest = new UpdateInvoiceRequest(
$invoice,
$modifyData['reason_of_correction'] ?? 'Abrechnungskorrektur',
$modifyData['type_internal'],
$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,
]);
}
}

View 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());
}
}

View 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'
]);
}
}

View File

@@ -0,0 +1,32 @@
<?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\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('/create', [CreateController::class, 'showForm']);
});
});
});

View File

@@ -0,0 +1,22 @@
<?php
use App\Domains\CostUnit\Controllers\CreateController;
use App\Domains\CostUnit\Controllers\ListController;
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('/create', [CreateController::class, 'showForm']);
});
});
});

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,117 @@
<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">
<button
v-if="props.data.status === 'new' && modeShow"
@click="emit('accept')"
class="button mareike-button mareike-accept-button"
>
Abrechnung annehmen
</button>
<button v-if="props.data.status === 'denied' && modeShow"
@click="emit('reopen')"
class="button mareike-button mareike-accept-button"
>
Abrechnung zur Wiedervorlage öffnen
</button>
<br />
<button
v-if="props.data.status === 'new' && modeShow"
@click="emit('fix')"
class="button mareike-button mareike-fix-button"
>
Abrechnung ablehnen und korrigieren
</button><br />
<button
v-if="props.data.status === 'new' && modeShow"
@click="emit('deny')"
class="button mareike-button mareike-deny-button"
>
Abrechnung ablehnen
</button>
</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -0,0 +1,158 @@
<script setup>
import { ref, onMounted, reactive } from 'vue'
import {checkFilesize} from "../../../../../../resources/js/components/InvoiceUploadChecks.js";
import RefundData from "./refund-data.vue";
import NumericInput from "../../../../../Views/Components/NumericInput.vue";
import AmountInput from "../../../../../Views/Components/AmountInput.vue";
import {useAjax} from "../../../../../../resources/js/components/ajaxHandler.js";
const data = defineProps({
eventId: Number,
userName: String,
userEmail: String,
userTelephone: String,
userIban: String,
userAccountOwner: String,
})
const { request } = useAjax();
const distanceAllowance = ref(null);
const travelDirection = ref(null);
const have_receipt = ref('')
const havePassengers = ref(false);
const materialTransportation = ref(false);
const amount = ref(0.00);
const invoiceType = ref(null);
const otherText = ref('');
const receipt = ref(null)
const finalStep = ref(false)
async function getDistanceAllowance() {
const tempData = await request('/api/v1/cost-unit/get-distance-allowance/' + data.eventId, {
method: "GET",
});
distanceAllowance.value = tempData.distanceAllowance;
have_receipt.value = 'no';
}
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;">Gib deine Reisesetrecke an</span></legend>
<input
type="text"
name="travel-direction"
v-model="travelDirection"
/>
</fieldset><br /><br />
<fieldset v-if="travelDirection !== null">
<legend><span style="font-weight: bolder;">Bist du mit dem ÖPNV gefahren oder besitzt du einen Beleg</span></legend>
<input type="button" style="border-radius: 0; width: 100px;" @click="have_receipt='yes'" value="Ja" />
<input type="button" style="border-radius: 0; width: 100px;" @click="getDistanceAllowance" value="Nein" />
</fieldset>
<br /><br />
<fieldset v-if="have_receipt === 'yes'">
<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
<br /><br />
<input type="button"
v-if="amount !== null && have_receipt === 'yes' && amount != '0'"
value="Beleg auswählen und fortfahren"
onclick="document.getElementById('receipt').click();"
/>
<input accept="application/pdf" type="file" id="receipt" name="receipt" @change="handleFileChange"
style="display: none"/>
<RefundData
v-if="finalStep && have_receipt === 'yes'"
:eventId="data.eventId"
invoice-type="travelling"
:amount="amount"
:other-text="travelDirection"
:materialTransportation="materialTransportation"
:havePassengers="havePassengers"
:userName="data.userName"
:userEmail="data.userEmail"
:userTelephone="data.userTelephone"
:userIban="data.userIban"
:userAccountOwner="data.userAccountOwner"
:receipt="receipt"
@close="finalStep = false"
/>
</fieldset>
<fieldset v-else-if="distanceAllowance != null">
<legend><span style="font-weight: bolder;">Reiseinformationen</span></legend>
Gesamtlänge des Reisewegs:
<NumericInput
class="width-small"
name="total_distance"
v-model="amount"
/> km<br />
<span style="font-weight: normal">({{ amount }} km x {{distanceAllowance.toFixed(2).replace('.', ',')}} Euro / km = <strong>{{ (amount * distanceAllowance).toFixed(2).replace('.', ',') }} Euro</strong>)</span>
<br /><br />
<input
type="checkbox"
name="havePassengers"
v-model="havePassengers"
id="havePassengers"
/> <label style="margin-bottom: 20px;" for="havePassengers">Ich habe Personen mitgenommen</label>
<br />
<input
type="checkbox"
name="materialTransportation"
v-model="materialTransportation"
id="materialTransportation"
/> <label style="margin-bottom: 20px;" for="materialTransportation">Ich habe Material transportiert</label>
<br /><br />
<input
v-if="amount !== null && have_receipt === 'no' && amount != '0'"
@click="finalStep = true;"
type="button"
value="Pauschalbetrag abrechnen" />
<RefundData
v-if="finalStep && have_receipt === 'no'"
:eventId="data.eventId"
invoice-type="travelling"
:amount="amount"
:other-text="travelDirection"
:materialTransportation="materialTransportation"
:havePassengers="havePassengers"
:userName="data.userName"
:userEmail="data.userEmail"
:userTelephone="data.userTelephone"
:userIban="data.userIban"
:userAccountOwner="data.userAccountOwner"
@close="finalStep = false"
/>
</fieldset>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Domains\MessageSystem\Actions\SendMessage;
use App\Enumerations\MessageType;
use Illuminate\Support\Facades\Mail;
class SendMessageCommand {
private SendMessageRequest $request;
public function __construct(SendMessageRequest $request) {
$this->request = $request;
}
public function send() : SendMessageResponse {
$response = new SendMessageResponse();
foreach ($this->request->messageTypes as $messageType) {
switch (true) {
case $messageType->value === MessageType::EMAIL->value:
$this->sendAsEmail();
break;
case $messageType->value === MessageType::INTERNAL->value:
$this->sendAsInternalMessage();
break;
}
}
return $response;
}
private function sendAsEmail() {
foreach ($this->request->recipient->getEmailAddresses() as $emailAddress) {
Mail::html($this->request->message, function ($message) use ($emailAddress) {
$message
->to($emailAddress->getValue(), $this->request->recipient->getName())
->subject($this->request->subject);
});
}
}
private function sendAsInternalMessage() {
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Domains\MessageSystem\Actions\SendMessage;
use App\Enumerations\MessageType;
use App\Models\User;
use App\ValueObjects\EmailAddress;
use App\ValueObjects\MessageRecipient;
class SendMessageRequest {
public string $message;
public string $subject;
public MessageRecipient $recipient;
/** @var MessageType[] */
public array $messageTypes;
public function __construct(
string $message,
string $subject,
MessageRecipient $recipient,
array $messageTypes
) {
$this->message = $message;
$this->recipient = $recipient;
$this->subject = $subject;
$this->messageTypes = $messageTypes;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Domains\MessageSystem\Actions\SendMessage;
class SendMessageResponse {
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Domains\UserManagement\Actions\GenerateActivationToken;
use App\Domains\MessageSystem\Actions\SendMessage\SendMessageCommand;
use App\Domains\MessageSystem\Actions\SendMessage\SendMessageRequest;
use App\Enumerations\MessageType;
use App\MessageTemplates\activationCodeTemplate;
use App\ValueObjects\EmailAddress;
use App\ValueObjects\MessageRecipient;
use Illuminate\Support\Str;
class GenerateActivationTokenCommand {
private GenerateActivationTokenRequest $request;
public function __construct(GenerateActivationTokenRequest $request) {
$this->request = $request;
}
public function execute() : GenerateActivationTokenResponse {
$response = new GenerateActivationTokenResponse;
$activationCode = Str::password(24,true,true,false,false);
$this->request->user->activation_token = $activationCode;
if (null !== $this->request->expirationDate) {
$this->request->user->activation_token_expires_at = $this->request->expirationDate;
}
$this->request->user->save();
$response->activationCode = $activationCode;
$activationMessage = new activationCodeTemplate();
$activationMessage->composeMessage(EmailAddress::fromString($this->request->user->email), $activationCode);
$recipient = new MessageRecipient();
$recipient->addEmailAddress(EmailAddress::fromString($this->request->user->email));
$recipient->setName($this->request->user->getFullname());
$message = activationCodeTemplate::createForUser(EmailAddress::fromString($this->request->user->email), $activationCode);
$userMessageRequests = new SendMessageRequest($message->getMessage(), $message->getSubject(), $recipient, [MessageType::EMAIL]);
$userMessageCommand = new SendMessageCommand($userMessageRequests);
$userMessageCommand->send();
return $response;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domains\UserManagement\Actions\GenerateActivationToken;
use App\Models\User;
class GenerateActivationTokenRequest {
public User $user;
public ?\DateTime $expirationDate;
public function __construct(User $user, ?\DateTime $expirationDate = null) {
$this->user = $user;
$this->expirationDate = $expirationDate;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Domains\UserManagement\Actions\GenerateActivationToken;
class GenerateActivationTokenResponse {
public string $activationCode;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Domains\UserManagement\Actions\UserActivation;
class UserActivationCommand {
private UserActivationRequest $request;
public function __construct(UserActivationRequest $request) {
$this->request = $request;
}
public function execute() : UserActivationResponse {
$response = new UserActivationResponse();
$this->request->user->active = true;
$this->request->user->activation_token = null;
$this->request->user->activation_token_expires_at = null;
$this->request->user->save();
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Domains\UserManagement\Actions\UserActivation;
use App\Models\User;
class UserActivationRequest {
public User $user;
public function __construct(User $user) {
$this->user = $user;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\UserManagement\Actions\UserActivation;
class UserActivationResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Domains\UserManagement\Actions\UserChangePassword;
class UserChangePasswordCommand {
private UserChangePasswordRequest $request;
public function __construct(UserChangePasswordRequest $request)
{
$this->request = $request;
}
public function execute() : UserChangePasswordResponse {
$response = new UserChangePasswordResponse();
$this->request->user->password = $this->request->newPassword;
$this->request->user->save();
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Domains\UserManagement\Actions\UserChangePassword;
use App\Models\User;
class UserChangePasswordRequest {
public User $user;
public string $newPassword;
public function __construct(User $user, string $newPassword) {
$this->user = $user;
$this->newPassword = $newPassword;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\UserManagement\Actions\UserChangePassword;
class UserChangePasswordResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Domains\UserManagement\Actions\UserActivation;
class UserDeactivationCommand {
private UserDeactivationRequest $request;
public function __construct(UserDeactivationRequest $request) {
$this->request = $request;
}
public function execute() : UserDeactivationResponse {
$response = new UserDeactivationResponse();
$this->request->user->active = false;
$this->request->user->password = NULL;
$this->request->user->username = 'deleted-' . $this->request->user->username;
$this->request->user->email = 'null@example.com';
$this->request->user->save();
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Domains\UserManagement\Actions\UserActivation;
use App\Models\User;
class UserDeactivationRequest {
public User $user;
public function __construct(User $user) {
$this->user = $user;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\UserManagement\Actions\UserActivation;
class UserDeactivationResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Domains\UserManagement\Actions\UserRegistration;
use App\Domains\MessageSystem\Actions\SendMessage\SendMessageCommand;
use App\Domains\MessageSystem\Actions\SendMessage\SendMessageRequest;
use App\Domains\UserManagement\Actions\GenerateActivationToken\GenerateActivationTokenCommand;
use App\Domains\UserManagement\Actions\GenerateActivationToken\GenerateActivationTokenRequest;
use App\Enumerations\MessageType;
use App\MessageTemplates\Registration\InformAdminAboutNewUserTemplate;
use App\Models\User;
use App\ValueObjects\EmailAddress;
use App\ValueObjects\MessageRecipient;
class UserRegistrationCommand {
private UserRegistrationRequest $request;
public function __construct(UserRegistrationRequest $request) {
$this->request = $request;
}
public function execute() : UserRegistrationResponse {
$response = new UserRegistrationResponse;
$user = User::create([
'user_role_main' => $this->request->userRoleMain,
'user_role_local_group' => $this->request->userRoleLocalGroup,
'username' => $this->request->email->getValue(),
'local_group' => $this->request->localGroup,
'firstname' => $this->request->firstname,
'lastname' => $this->request->lastname,
'nickname' => $this->request->nickname !== '' ? $this->request->nickname : null,
'email' => $this->request->email->getValue(),
]);
if ($user === null) {
return $response;
}
$generateActivationCoedeRequest = new GenerateActivationTokenRequest($user);
$generateActivationCoedeDommand = new GenerateActivationTokenCommand($generateActivationCoedeRequest);
$result = $generateActivationCoedeDommand->execute();
$user->activation_token = $result->activationCode;
$siteAdmin = new MessageRecipient();
$siteAdmin->addEmailAddress(EmailAddress::fromString(env('APP_ADMIN_MAIL')));
$siteAdmin->setName(env('APP_ADMIN_NAME'));
$registrationMessage = InformAdminAboutNewUserTemplate::createNew($user);
$registrationMessageRequest = new SendMessageRequest($registrationMessage->getMessage(), $registrationMessage->getSubject(), $siteAdmin, [MessageType::EMAIL]);
$registrationMessageCommand = new SendMessageCommand($registrationMessageRequest);
$registrationMessageCommand->send();
$response->user = $user;
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Domains\UserManagement\Actions\UserRegistration;
use App\ValueObjects\EmailAddress;
class UserRegistrationRequest {
public string $firstname;
public string $lastname;
public EmailAddress $email;
public string $nickname;
public string $userRoleMain;
public string $userRoleLocalGroup;
public string $localGroup;
public function __construct(string $firstname, string $lastname, string $nickname, EmailAddress $email, string $userRoleMain, string $userRoleLocalGroup, string $localGroup) {
$this->firstname = $firstname;
$this->lastname = $lastname;
$this->nickname = $nickname;
$this->email = $email;
$this->userRoleMain = $userRoleMain;
$this->userRoleLocalGroup = $userRoleLocalGroup;
$this->localGroup = $localGroup;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Domains\UserManagement\Actions\UserRegistration;
use App\Models\User;
class UserRegistrationResponse {
public ?User $user;
public bool $success;
public function __construct() {
$this->user = null;
$this->success = false;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Domains\UserManagement\Controllers;
use App\Domains\UserManagement\Actions\UserActivation\UserActivationCommand;
use App\Domains\UserManagement\Actions\UserActivation\UserActivationRequest;
use App\Domains\UserManagement\Actions\UserActivation\UserDeactivationCommand;
use App\Domains\UserManagement\Actions\UserActivation\UserDeactivationRequest;
use App\Domains\UserManagement\Actions\UserChangePassword\UserChangePasswordCommand;
use App\Domains\UserManagement\Actions\UserChangePassword\UserChangePasswordRequest;
use App\Domains\UserManagement\Actions\UserRegistration\UserRegistrationCommand;
use App\Domains\UserManagement\Actions\UserRegistration\UserRegistrationRequest;
use App\Enumerations\UserRole;
use App\Models\User;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use App\ValueObjects\EmailAddress;
use Carbon\Traits\Date;
use DateTime;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EmailVerificationController extends CommonController
{
public function verifyEmailForm(Request $request) {
$inertiaProvider = new InertiaProvider('UserManagement/VerifyEmail', ['appName' => app('tenant')->name]);
return $inertiaProvider->render();
}
public function doVerification(Request $request) : JsonResponse
{
$user = $this->users->findByUsername($request->get('email'));
if ($user === null) {
return response()->json([
'error_types' => [
'email' => 'Die E-Mail-Adresse konnte nicht zugeordnet werden.',
],
]);
}
if (new DateTime() > DateTime::createFromFormat('Y-m-d H:i:s', $user->activation_token_expires_at)) {
return response()->json([
'error_types' => [
'verificationToken' => 'Der Sicherheitsschlüssel ist abgelaufen.',
],
]);
}
if (!$this->users->checkVerificationToken($user, $request->get('verificationToken'))) {
return response()->json([
'error_types' => [
'verificationToken' => 'Der Sicherheitsschlüssel ist nicht korrekt',
],
]);
}
$userActivationRequest = new UserActivationRequest($user);
$userActivationCommand = new UserActivationCommand($userActivationRequest);
$activationResult = $userActivationCommand->execute();
if (!$activationResult->success) {
return response()->json([
'error_types' => [
'verificationToken' => 'Ein allgemeiner Fehler ist aufgetreten. Bitte versuche es später noch einmal.',
],
]);
}
$userPasswordResetRequest = new UserChangePasswordRequest($user, $request->get('password'));
$userPasswordResetCommand = new UserChangePasswordCommand($userPasswordResetRequest);
$userPasswordResetCommand->execute();
return response()->json([
'status' => 'success',
'message' => 'Dein Account wurde aktiviert.Du kannst dich nun mit deinem neuen Passwort einloggen.'
]);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Domains\UserManagement\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LogOutController {
public function __invoke(Request $request) {
Auth::logout();
// Session invalidieren
$request->session()->invalidate();
// CSRF-Token regenerieren (für Sicherheit)
$request->session()->regenerateToken();
// Redirect z.B. zur Login-Seite
return redirect()->intended('/')->with('status', 'Erfolgreich abgemeldet!');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Domains\UserManagement\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends CommonController {
public function loginForm(Request $request) {
$errors = [];
if ($request->session()->has('errors')) {
$errors = $request->session()->get('errors')->getBag('default')->getMessages();
}
$inertiaProvider = new InertiaProvider('UserManagement/Login', ['errors' => $errors, 'appName' => app('tenant')->name]);
return $inertiaProvider->render();
}
public function doLogin(Request $request)
{
$credentials = $request->validate([
'username' => ['required', 'string'],
'password' => ['required'],
],
[
'username.required' => 'Bitte gib deinen Anmeldenamen ein.',
'username.string' => 'Der Anmeldename muss eine E-Mail-Adresse sein.',
'password.required' => 'Bitte gib dein Passwort ein.',
]);
$user = $this->users->findByUsername($request->get('username'));
if ($user !== null && $user->password === null) {
return redirect()->intended('/register/verifyEmail');
}
#$credentials = ['username' => 'development', 'password' => 'development'];
if (!Auth::attempt($credentials)) {
return back()->withErrors([
'username' => 'Diese Zugangsdaten sind ungültig.',
]);
}
$request->session()->regenerate();
$user = Auth::user();
# dd($user->firstname . ' ' . $user->lastname);
return redirect()->intended('/');
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Domains\UserManagement\Controllers;
use App\Domains\MessageSystem\Actions\SendMessage\SendMessageCommand;
use App\Domains\MessageSystem\Actions\SendMessage\SendMessageRequest;
use App\Domains\UserManagement\Actions\UserRegistration\UserRegistrationCommand;
use App\Domains\UserManagement\Actions\UserRegistration\UserRegistrationRequest;
use App\Enumerations\MessageType;
use App\Enumerations\UserRole;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use App\ValueObjects\EmailAddress;
use App\ValueObjects\MessageRecipient;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class RegistrationController extends CommonController {
public function loginForm(Request $request) {
$errors = [];
if ($request->session()->has('errors')) {
$errors = $request->session()->get('errors')->getBag('default')->getMessages();
}
$inertiaProvider = new InertiaProvider('UserManagement/Registration', [
'errors' => $errors,
'appName' => app('tenant')->name,
'tenant' => app('tenant'),
]);
return $inertiaProvider->render();
}
public function doRegistration(Request $request) : JsonResponse {
$user = $this->users->findByUsername($request->get('email'));
if ($user !== null) {
return response()->json([
'status' => 'error',
'message' => 'Dieser Account existiert bereits.'
]);
}
$email = EmailAddress::fromString($request->get('email'));
$userRoleMain = UserRole::USER_ROLE_USER;
$userRoleLocalGroup = UserRole::USER_ROLE_USER;
$localGroup = app('tenant')->slug === 'lv' ? $request->get('localGroup') : app('tenant')->slug;
$registrationRequest = new UserRegistrationRequest(
$request->get('firstname'),
$request->get('lastname'),
$request->get('nickname'),
$email,
$userRoleMain,
$userRoleLocalGroup,
$localGroup
);
$registrationCommand = new UserRegistrationCommand($registrationRequest);
$result = $registrationCommand->execute();
if (!$result->success) {
return response()->json([
'status' => 'error',
'message' => 'Beim Erstellen des Accounts ist ein Fehler aufgetreten.'
]);
}
return response()->json([
'status' => 'success',
'message' => 'Registrierung erfolgreich! Bitte prüfe nun dein E-Mail-Postfach'
]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Domains\UserManagement\Controllers;
use App\Domains\UserManagement\Actions\GenerateActivationToken\GenerateActivationTokenCommand;
use App\Domains\UserManagement\Actions\GenerateActivationToken\GenerateActivationTokenRequest;
use App\Domains\UserManagement\Actions\GenerateActivationToken\GenerateActivationTokenResponse;
use App\Domains\UserManagement\Actions\UserActivation\UserActivationCommand;
use App\Domains\UserManagement\Actions\UserActivation\UserActivationRequest;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ResetPasswordController extends CommonController {
public function resetPasswordForm() {
$inertiaProvider = new InertiaProvider('UserManagement/ResetPassword', []);
return $inertiaProvider->render();
}
public function doResetPassword(Request $request) : JsonResponse {
$user = $this->users->findByUsername($request->get('email'));
if (null !== $user) {
$expirationDate = new \DateTime()->modify('+15 Minutes');
$resetAccountRequest = new GenerateActivationTokenRequest($user, $expirationDate);
$resetAccountCommand = new GenerateActivationTokenCommand($resetAccountRequest);
$resetAccountCommand->execute();
}
return response()->json([
'status' => 'success',
'message' => 'Falls deine E-Mail-Adresse gefunden wurde, erhältst du nun eine E-Mail mit weiteren Schritten zum Zurücksetzen deines Passwortes.'
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php
use App\Domains\UserManagement\Controllers\EmailVerificationController;
use App\Domains\UserManagement\Controllers\RegistrationController;
use App\Domains\UserManagement\Controllers\ResetPasswordController;
use App\Middleware\IdentifyTenant;
use App\Providers\GlobalDataProvider;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::prefix('v1')
->group(function () {
Route::middleware(IdentifyTenant::class)->group(function () {
});
});

View File

@@ -0,0 +1,28 @@
<?php
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::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']);
Route::middleware(['auth'])->group(function () {
Route::post('/logout', [LogoutController::class, 'logout']);
});
});

View File

@@ -0,0 +1,69 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import {onMounted, ref} from 'vue'
import { toast } from 'vue3-toastify'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
const props = defineProps({
navbar: Object,
tenant: String,
user: Object,
currentPath: String,
errors: Object,
})
onMounted(() => {
if (undefined !== props.errors && undefined !== props.errors.username) {
toast.error(props.errors.username[0])
}
})
//console.log(props.errors.password[0])
const username = ref('')
const password = ref('')
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
function resetPassword() {
window.location.href = '/reset-password';
}
</script>
<template>
<AppLayout title='Anmelden' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
<form method="POST" action="/login">
<input type="hidden" name="_token" :value="csrfToken" />
<shadowed-box style="width: 50%; margin: 150px auto; padding: 20px;">
<table>
<tr>
<th>Anmeldename</th>
<td>
<input type="text" name="username" id="username"></input>
</td>
</tr>
<tr>
<th>Passwort</th>
<td><input type="password" name="password" id="password"></input></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Anmelden" style="margin-top: 20px;" />
<input type="button" @click="resetPassword" style="margin-top: 20px; margin-left: 20px;" value="Passwort vergessen" />
</td>
</tr>
</table>
</shadowed-box>
</form>
</AppLayout>
</template>
<style>
th {
width: 100px;
}
</style>

View File

@@ -0,0 +1,166 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import {computed, onMounted, reactive, ref} from 'vue'
import { toast } from 'vue3-toastify'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import {useAjax} from "../../../../resources/js/components/ajaxHandler.js";
import ErrorText from "../../../Views/Components/ErrorText.vue";
const props = defineProps({
navbar: Object,
tenant: String,
user: Object,
currentPath: String,
errors: Object,
availableLocalGroups: Array,
})
onMounted(() => {
if (undefined !== props.errors && undefined !== props.errors.username) {
toast.error(props.errors.username[0])
}
})
//console.log(props.errors.password[0])
const username = ref('')
const password = ref('')
const form = reactive({
firstname: '',
lastname: '',
nickname: '',
email: '',
localGroup: '',
password: '',
password_confirmation: ''
})
const errors = reactive({})
const { request } = useAjax()
const isValid = computed(() => {
errors.firstname = ''
errors.lastname = ''
errors.localGroup = ''
errors.email = ''
if (!form.firstname) {
errors.firstname = 'Bitte gib deinen Vornamen ein'
}
if (!form.lastname) {
errors.lastname = 'Bitte gib deinen Nachnamen ein'
}
if (!form.localGroup) {
errors.localGroup = 'Bitte gib deinen Stamm an'
}
if (!form.email) {
errors.email = 'Bitte gib deine E-Mail-Adresse ein'
} else if (!/^\S+@\S+\.\S+$/.test(form.email)) {
errors.email = 'Ungültige E-Mail'
}
return !errors.password && !errors.firstname && !errors.lastname && !errors.localGroup
})
async function submit() {
if (!isValid.value) return false
const data = await request("/api/v1/register", {
method: "POST",
body: {
"firstname": form.firstname,
"lastname": form.lastname,
'nickname': form.nickname,
"email": form.email,
"localGroup": form.localGroup,
}
});
if (data.status === 'error') {
toast.error(data.message);
} else {
toast.success(data.message)
window.location.href = '/register/verifyEmail';
}
return;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
</script>
<template>
<AppLayout title='Registrieren'>
<form method="POST" action="/register" @submit.prevent="submit">
<input type="hidden" name="_token" :value="csrfToken" />
<shadowed-box style="width: 75%; margin: 150px auto; padding: 20px;">
<table>
<tr>
<th>Vorname</th>
<td>
<input type="text" name="firstname" id="firstname" v-model="form.firstname" />
<ErrorText :message="errors.firstname" />
</td>
</tr>
<tr>
<th>Nachname</th>
<td>
<input type="text" name="lastname" id="lastname" v-model="form.lastname" />
<ErrorText :message="errors.lastname" />
</td>
</tr>
<tr>
<th>
Pfadi-Name*<br />
<small>*Falls vorhanden</small>
</th>
<td>
<input type="text" name="nickname" id="nickname" v-model="form.nickname" />
</td>
</tr>
<tr>
<th>E-Mail-Adresse</th>
<td>
<input type="email" name="email" id="email" v-model="form.email" />
<ErrorText :message="errors.email" />
</td>
</tr>
<tr v-if="props.tenant.slug === 'lv'">
<th>Stamm</th>
<td>
<select name="localgroup" v-model="form.localGroup">
<option v-for="localGroup in props.availableLocalGroups" :value="localGroup.slug">{{localGroup.name}}</option>
</select>
<ErrorText :message="errors.localGroup" />
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Registrieren" style="margin-top: 20px;" />
</td>
</tr>
</table>
</shadowed-box>
</form>
</AppLayout>
</template>
<style>
th {
width: 150px;
}
</style>

View File

@@ -0,0 +1,102 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import {computed, onMounted, reactive, ref} from 'vue'
import { toast } from 'vue3-toastify'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import {useAjax} from "../../../../resources/js/components/ajaxHandler.js";
import ErrorText from "../../../Views/Components/ErrorText.vue";
const props = defineProps({
navbar: Object,
tenant: String,
user: Object,
currentPath: String,
errors: Object,
availableLocalGroups: Array,
})
onMounted(() => {
if (undefined !== props.errors && undefined !== props.errors.username) {
toast.error(props.errors.username[0])
}
})
const form = reactive({
email: '',
verificationToken: '',
})
const errors = reactive({})
const { request } = useAjax()
const isValid = computed(() => {
errors.email = ''
if (!form.email) {
errors.email = 'Bitte gib deine E-Mail-Adresse ein'
}
return !errors.email
})
async function submit() {
if (!isValid.value) return false
const data = await request("/api/v1/reset-password", {
method: "POST",
body: {
"email": form.email,
}
});
if (data.error_types) {
Object.keys(data.error_types).forEach((key) => {
if (key in errors) {
errors[key] = data.error_types[key]
}
});
} else {
window.location.href = '/register/verifyEmail';
toast.success(data.message)
}
return;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
</script>
<template>
<AppLayout title='Passwort zurücksetzen' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
<form method="POST" action="/reset-password" @submit.prevent="submit">
<input type="hidden" name="_token" :value="csrfToken" />
<shadowed-box style="width: 75%; margin: 150px auto; padding: 20px;">
<table>
<tr>
<th>E-Mail-Adresse</th>
<td>
<input type="email" name="email" id="email" v-model="form.email" />
<ErrorText :message="errors.email" />
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Verifizierung starten" style="margin-top: 20px;" />
</td>
</tr>
</table>
</shadowed-box>
</form>
</AppLayout>
</template>
<style>
th {
width: 150px;
}
</style>

View File

@@ -0,0 +1,152 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import {computed, onMounted, reactive, ref} from 'vue'
import { toast } from 'vue3-toastify'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import {useAjax} from "../../../../resources/js/components/ajaxHandler.js";
import ErrorText from "../../../Views/Components/ErrorText.vue";
const props = defineProps({
navbar: Object,
tenant: String,
user: Object,
currentPath: String,
errors: Object,
availableLocalGroups: Array,
})
onMounted(() => {
if (undefined !== props.errors && undefined !== props.errors.username) {
toast.error(props.errors.username[0])
}
})
const form = reactive({
email: '',
verificationToken: '',
password: '',
})
const errors = reactive({})
const { request } = useAjax()
const isValid = computed(() => {
errors.email = '';
errors.verificationToken = '';
errors.password = '';
if (!form.email) {
errors.email = 'Bitte gib deine E-Mail-Adresse ein'
} else if (!/^\S+@\S+\.\S+$/.test(form.email)) {
errors.email = 'Ungültige E-Mail'
}
if (!form.verificationToken) {
errors.verificationToken = 'Bitte gib den Sicherheitsschlüssel, den du per E-Mail erhalten hast, ein'
}
if (!form.password) {
errors.password = 'Bitte gib ein Passwort ein'
} else if (form.password.length < 2) {
errors.password = 'Das Passwort muss mindestens 12 Zeichen lang sein'
}
if (form.password !== form.password_confirmation) {
errors.password = 'Das Passwort und die Wiederholung stimmen nicht überein'
}
return !errors.password && !errors.email && !errors.verificationToken
})
async function submit() {
if (!isValid.value) return false
const data = await request("/api/v1/register/confirmEmail", {
method: "POST",
body: {
"email": form.email,
"verificationToken": form.verificationToken,
"password": form.password,
}
});
if (data.error_types) {
Object.keys(data.error_types).forEach((key) => {
if (key in errors) {
errors[key] = data.error_types[key]
}
});
} else {
toast.success(data.message)
window.location.href = '/login';
}
return;
}
function resetPassword() {
window.location.href = '/reset-password';
}
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
</script>
<template>
<AppLayout title='E-Mail bestätigen' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
<form method="POST" action="/register" @submit.prevent="submit">
<input type="hidden" name="_token" :value="csrfToken" />
<shadowed-box style="width: 75%; margin: 150px auto; padding: 20px;">
<p>
Bitte prüfe dein E-Mail-Postfach.<br />
Solltest du keinen Aktivierungsschlüssel erhalten haben, kontaktiere bitte einen Administrator.
</p>
<table>
<tr>
<th>E-Mail-Adresse</th>
<td>
<input type="email" name="email" id="email" v-model="form.email" />
<ErrorText :message="errors.email" />
</td>
</tr>
<tr>
<th>Sicherheitsschlüssel</th>
<td>
<input type="text" name="verificationToken" id="verificationToken" v-model="form.verificationToken" />
<ErrorText :message="errors.verificationToken" />
</td>
</tr>
<tr>
<th>Dein neues Passwort</th>
<td><input type="password" name="password" id="password" v-model="form.password" /></td>
</tr>
<tr>
<th>Passwort (wiederholen)</th>
<td>
<input type="password" name="password_retype" id="password_retype" v-model="form.password_confirmation" />
<ErrorText :message="errors.password" />
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="E-Mail validieren" style="margin-top: 20px;" />
<input type="button" @click="resetPassword" style="margin-top: 20px; margin-left: 20px;" value="Validierungscode neu zusenden" />
</td>
</tr>
</table>
</shadowed-box>
</form>
</AppLayout>
</template>
<style>
th {
width: 150px;
}
</style>

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* @property string $slug
* @property string $name
*/
class CostUnitType extends CommonModel
{
public const COST_UNIT_TYPE_EVENT = 'event';
public const COST_UNIT_TYPE_RUNNING_JOB = 'running_job';
protected $fillable = [
'slug',
'name',
];
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* @property string $slug
* @property string $name
*/
class EatingHabit extends CommonModel
{
public const string EATING_HABIT_OMNIVOR = 'EATING_HABIT_OMNIVOR';
public const string EATING_HABIT_VEGETARIAN = 'EATING_HABIT_VEGETARIAN';
public const string EATING_HABIT_VEGAN = 'EATING_HABIT_VEGAN';
protected $fillable = [
'slug',
'name',
];
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
/**
* @property string $slug
* @property string $name
*/
class FirstAidPermission extends CommonModel
{
public const string FIRST_AID_PERMISSION_ALLOWED = 'FIRST_AID_PERMISSION_ALLOWED';
public const string FIRST_AID_PERMISSION_DENIED = 'FIRST_AID_PERMISSION_DENIED';
protected $fillable = [
'slug',
'name',
'description'
];
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
class InvoiceStatus extends CommonModel
{
public const INVOICE_STATUS_NEW = 'new';
public const INVOICE_STATUS_APPROVED = 'approved';
public const INVOICE_STATUS_DENIED = 'denied';
public const INVOICE_STATUS_EXPORTED = 'exported';
public const INVOICE_STATUS_DELETED = 'deleted';
public const INVOICE_META_STATUS_NO_PAYOUT = 'no_payout';
public const INVOICE_META_STATUS_DONATED = 'donated';
protected $table = 'invoice_status';
protected $fillable = [
'slug',
];
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
class InvoiceType extends CommonModel {
public const INVOICE_TYPE_TRAVELLING = 'travelling';
public const INVOICE_TYPE_PROGRAM = 'program';
public const INVOICE_TYPE_OTHER = 'other';
public const INVOICE_TYPE_ACCOMMODATION = 'accommodation';
public const INVOICE_TYPE_CATERING = 'catering';
protected $fillable = [
'slug',
'name',
];
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Enumerations;
enum MessageType : string {
case INTERNAL = 'INTERNAL';
case EMAIL = 'EMAIL';
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* @property string $slug
* @property string $name
*/
class SwimmingPermission extends CommonModel
{
public const string SWIMMING_PERMISSION_ALLOWED = 'SWIMMING_PERMISSION_ALLOWED';
public const string SWIMMING_PERMISSION_LIMITED = 'SWIMMING_PERMISSION_LIMITED';
public const string SWIMMING_PERMISSION_DENIED = 'SWIMMING_PERMISSION_DENIED';
use HasFactory;
protected $fillable = [
'slug',
'name',
];
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
class UserRole extends CommonModel {
public const USER_ROLE_ADMIN = 'ROLE_ADMINISTRATOR';
public const USER_ROLE_GROUP_LEADER = 'ROLE_GROUP_LEADER';
public const USER_ROLE_USER = 'ROLE_USER';
protected $fillable = [
'name',
'slug'
];
}

View File

@@ -1,8 +0,0 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use App\Providers\InertiaProvider;
class TestRenderInertiaProvider
{
public function index() {
$inertiaProvider = new InertiaProvider('Invoice/CreateInvoice', ['appName' => app('tenant')->name]);
return $inertiaProvider->render();
}
}

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