Cost units can be edited

This commit is contained in:
2026-02-08 20:06:38 +01:00
parent 6fc65e195c
commit bccfc11687
53 changed files with 2021 additions and 29 deletions

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->tresurers()->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,35 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitCommand;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
use App\Enumerations\CostUnitType;
use App\Models\CostUnit;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CreateController extends CommonController{
public function showForm() {
$inertiaProvider = new InertiaProvider('CostUnit/Create', []);
return $inertiaProvider->render();
}
public function createCostUnitRunningJob(Request $request) : JsonResponse {
$createCostUnitRequest = new CreateCostUnitRequest(
$request->get('cost_unit_name'),
CostUnitType::COST_UNIT_TYPE_RUNNING_JOB,
Amount::fromString($request->get('distance_allowance')),
$request->get('mail_on_new')
);
$createCostUnitCommand = new CreateCostUnitCommand($createCostUnitRequest);
$result = $createCostUnitCommand->execute();
session()->put('message', 'Die laufende Tätigkeit wurde erfolgreich angelegt.');
return response()->json([]);
}
}

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,50 @@
<?php
namespace App\Domains\CostUnit\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListController extends CommonController {
public function __invoke() {
$inertiaProvider = new InertiaProvider('CostUnit/List', [
'cost_unit_id' => 1
]);
return $inertiaProvider->render();
}
public function listCurrentEvents(Request $request) : JsonResponse {
return response()->json([
'cost_unit_title' => 'Aktuelle Veranstaltungen',
'cost_units' => $this->costUnits->getCurrentEvents(),
]);
}
public function listCurrentRunningJobs(Request $request) : JsonResponse {
return response()->json([
'cost_unit_title' => 'Laufende Tätigkeiten',
'cost_units' => $this->costUnits->getRunningJobs(),
]);
}
public function listClosedCostUnits(Request $request) : JsonResponse {
return response()->json([
'cost_unit_title' => 'Geschlossene Kostenstellen',
'cost_units' => $this->costUnits->getClosedCostUnits(),
]);
}
public function listArchivedCostUnits(Request $request) : JsonResponse {
return response()->json([
'cost_unit_title' => 'Archivierte Kostenstellen',
'cost_units' => $this->costUnits->getArchivedCostUnits(),
]);
}
}

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,49 @@
<?php
use App\Domains\CostUnit\Controllers\ChangeStateController;
use App\Domains\CostUnit\Controllers\CreateController;
use App\Domains\CostUnit\Controllers\EditController;
use App\Domains\CostUnit\Controllers\ListController;
use App\Domains\CostUnit\Controllers\TreasurersEditController;
use App\Domains\UserManagement\Controllers\EmailVerificationController;
use App\Domains\UserManagement\Controllers\RegistrationController;
use App\Domains\UserManagement\Controllers\ResetPasswordController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::prefix('api/v1')
->group(function () {
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('cost-unit')->group(function () {
Route::middleware(['auth'])->group(function () {
Route::post('/create-running-job', [CreateController::class, 'createCostUnitRunningJob']);
Route::prefix('/{costUnitId}') ->group(function () {
Route::post('/close', [ChangeStateController::class, 'close']);
Route::post('/open', [ChangeStateController::class, 'open']);
Route::post('/archive', [ChangeStateController::class, 'archive']);
Route::get('/details', EditController::class);
Route::post('/details', [EditController::class, 'update']);
Route::get('/treasurers', TreasurersEditController::class);
Route::post('/treasurers', [TreasurersEditController::class, 'update']);
});
Route::prefix('open')->group(function () {
Route::get('/current-events', [ListController::class, 'listCurrentEvents']);
Route::get('/current-running-jobs', [ListController::class, 'listCurrentRunningJobs']);
Route::get('/closed-cost-units', [ListController::class, 'listClosedCostUnits']);
Route::get('/archived-cost-units', [ListController::class, 'listArchivedCostUnits']);
});
});
});
});
});

View File

@@ -0,0 +1,39 @@
<?php
use App\Domains\CostUnit\Controllers\CreateController;
use App\Domains\CostUnit\Controllers\ListController;
use App\Domains\Dashboard\Controllers\DashboardController;
use App\Domains\UserManagement\Controllers\EmailVerificationController;
use App\Domains\UserManagement\Controllers\LoginController;
use App\Domains\UserManagement\Controllers\LogOutController;
use App\Domains\UserManagement\Controllers\RegistrationController;
use App\Domains\UserManagement\Controllers\ResetPasswordController;
use App\Http\Controllers\TestRenderInertiaProvider;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('cost-unit')->group(function () {
Route::middleware(['auth'])->group(function () {
Route::get('/create', [CreateController::class, 'showForm']);
Route::get('/list', ListController::class);
});
});
Route::get('/register', [RegistrationController::class, 'loginForm']);
Route::get('/register/verifyEmail', [EmailVerificationController::class, 'verifyEmailForm']);
Route::get('/reset-password', [ResetPasswordController::class, 'resetPasswordForm']);
route::get('/logout', LogOutController::class);
route::post('/login', [LoginController::class, 'doLogin']);
route::get('/login', [LoginController::class, 'loginForm']);
});

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,84 @@
<script setup>
import {reactive, inject, onMounted} from 'vue';
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import {toast} from "vue3-toastify";
import ListCostUnits from "./Partials/ListCostUnits.vue";
const props = defineProps({
message: String,
data: {
type: [Array, Object],
default: () => []
},
cost_unit_id: {
type: Number,
default: 0
},
invoice_id: {
type: Number,
default: 0
}
})
// Prüfen, ob ein ?id= Parameter in der URL übergeben wurde
const urlParams = new URLSearchParams(window.location.search)
const initialCostUnitId = props.cost_unit_id
const initialInvoiceId = props.invoice_id
const tabs = [
{
title: 'Aktuelle Veranstaltungen',
component: ListCostUnits,
endpoint: "/api/v1/cost-unit/open/current-events",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Laufende Tätigkeiten',
component: ListCostUnits,
endpoint: "/api/v1/cost-unit/open/current-running-jobs",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Geschlossene Kostenstellen',
component: ListCostUnits,
endpoint: "/api/v1/cost-unit/open/closed-cost-units",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
{
title: 'Archivierte Kostenstellen',
component: ListCostUnits,
endpoint: "/api/v1/cost-unit/open/archived-cost-units",
deep_jump_id: initialCostUnitId,
deep_jump_id_sub: initialInvoiceId,
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<AppLayout title="Kostenstellen">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" :initial-tab-id="initialCostUnitId" :initial-sub-tab-id="initialInvoiceId" />
</shadowed-box>
</AppLayout>
</template>

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,255 @@
<script setup>
import {createApp, ref} from 'vue'
/*import {
_mareike_download_as_zip,
_mareike_use_webdav
} from "../../../assets/javascripts/library";*/
//import LoadingModal from "../../../assets/components/LoadingModal.vue";
//import Invoices from '../invoices/index.vue'
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import CostUnitDetails from "./CostUnitDetails.vue";
import {toast} from "vue3-toastify";
import Treasurers from "./Treasurers.vue";
//import CostUnitDetails from "./CostUnitDetails.vue";
const props = defineProps({
data: {
type: [Array, Object],
default: () => []
},
deep_jump_id: {
type: Number,
default: 0
},
deep_jump_id_sub: {
type: Number,
default: 0
}
})
const showInvoiceList = ref(false)
const invoices = ref(null)
const current_cost_unit = ref(null)
const showLoading = ref(false)
const show_invoice = ref(false)
const invoice = ref(null)
const show_cost_unit = ref(false)
const showTreasurers = ref(false)
const costUnit = ref(null)
const { data, loading, error, request, download } = useAjax()
if (props.deep_jump_id > 0) {
open_invoice_list(props.deep_jump_id, 'new', props.deep_jump_id_sub)
}
async function costUnitDetails(costUnitId) {
const data = await request('/api/v1/cost-unit/' + costUnitId + '/details', {
method: "GET",
});
showLoading.value = false;
if (data.status === 'success') {
costUnit.value = data.costUnit
show_cost_unit.value = true
} else {
toast.error(data.message);
}
}
async function editTreasurers(costUnitId) {
const data = await request('/api/v1/cost-unit/' + costUnitId + '/treasurers', {
method: "GET",
});
showLoading.value = false;
if (data.status === 'success') {
costUnit.value = data.costUnit
showTreasurers.value = true
} else {
toast.error(data.message);
}
}
async function open_invoice_list(cost_unit_id, endpoint, invoice_id) {
const url = '' // `/wp-json/mareike/invoices/list-${endpoint}?invoice_id=${invoice_id}&cost_unit_id=${cost_unit_id}
try {
const response = await fetch(url, { method: 'GET' })
if (!response.ok) throw new Error('Fehler beim Laden')
invoices.value = await response.json()
current_cost_unit.value = cost_unit_id
invoice_id = invoice_id
showInvoiceList.value = true
} catch (err) {
}
}
async function denyNewRequests(costUnitId) {
changeCostUnitState(costUnitId, 'close');
}
async function archiveCostUnit(costUnitId) {
changeCostUnitState(costUnitId, 'archive');
}
async function allowNewRequests(costUnitId) {
changeCostUnitState(costUnitId, 'open');
}
async function changeCostUnitState(costUnitId, endPoint) {
showLoading.value = true;
const data = await request('/api/v1/cost-unit/' + costUnitId + '/' + endPoint, {
method: "POST",
});
showLoading.value = false;
if (data.status === 'success') {
toast.success(data.message);
document.getElementById('costUnitBox_' + costUnitId).style.display = 'none';
} else {
toast.error(data.message);
}
}
async function export_payouts(cost_unit_id) {
showLoading.value = true;
try {
if (_mareike_download_as_zip()) {
const response = await fetch("/wp-json/mareike/costunits/export-payouts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
mareike_nonce: _mareike_nonce(),
costunit: cost_unit_id,
}),
});
if (!response.ok) throw new Error('Fehler beim Export (ZIP)');
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = downloadUrl;
a.download = `payouts-${cost_unit_id}.zip`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(downloadUrl);
} else {
await request("/wp-json/mareike/costunits/export-payouts", {
method: "POST",
body: {
mareike_nonce: _mareike_nonce(),
costunit: cost_unit_id,
}
});
}
showLoading.value = false;
toast.success('Die Abrechnungen wurden exportiert.');
} catch (err) {
showLoading.value = false;
toast.error('Beim Export der Abrechnungen ist ein Fehler aufgetreten.');
}
}
</script>
<template>
<div v-if="props.data.cost_units && props.data.cost_units.length > 0 && !showInvoiceList">
<h2>{{ props.data.cost_unit_title }}</h2>
<span v-for="costUnit in props.data.cost_units" class="costunit-list" :id="'costUnitBox_' + costUnit.id">
<table style="width: 100%">
<thead>
<tr><td colspan="5">
{{ costUnit.name }}
</td></tr>
</thead>
<tr>
<th>Gesamtbeitrag</th>
<td>{{ costUnit.totalAmount }}</td>
<th>Unbearbeitet</th>
<td>{{ costUnit.countNewInvoices }}</td>
<td rowspan="4" style="vertical-align: top;">
<input v-if="!costUnit.archived" type="button" value="Abrechnungen bearbeiten" />
<input v-else type="button" value="Abrechnungen einsehen" />
<br />
<input v-if="!costUnit.archived" type="button" value="Genehmigte Abrechnungen exportieren" style="margin-top: 10px;" />
</td>
</tr>
<tr>
<th>Spenden</th>
<td>{{ costUnit.donatedAmount }}</td>
<th>Nicht exportiert</th>
<td>{{ costUnit.countApprovedInvoices }}</td>
</tr>
<tr>
<td colspan="2"></td>
<th>Ohne Auszahlung</th>
<td colspan="2">{{ costUnit.countDonatedInvoices }}</td>
</tr>
<tr>
<td colspan="2"></td>
<th>Abgelehnt</th>
<td colspan="2">{{ costUnit.countDeniedInvoices }}</td>
</tr>
<tr>
<td colspan="5" style="width: 100%; padding-top: 20px;">
<strong @click="costUnitDetails(costUnit.id)" v-if="costUnit.allow_new" class="link">Details anpassen</strong> &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,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>