Implemented Event Budget

This commit is contained in:
2026-05-26 18:12:42 +02:00
parent 575fb27018
commit 28ffbdb696
17 changed files with 422 additions and 143 deletions
@@ -0,0 +1,50 @@
<?php
namespace App\Domains\Budget\Actions\CreateEstimate;
use App\Models\CostUnitEstimate;
class CreateEstimateAction {
private CreateEstimateResponse $response;
public function __construct(private CreateEstimateRequest $request) {
}
public function execute(): CreateEstimateResponse {
$this->response = new CreateEstimateResponse();
$amount = [];
switch ($this->request->amountType) {
case 'flat':
$amount['flat_amount'] = $this->request->amount;
break;
case 'per_person':
$amount['amount_by_user'] = $this->request->amount;
break;
}
if ($this->request->estimateId === 0) {
$estimate = CostUnitEstimate::create(array_merge([
'tenant' => app('tenant')->slug,
'cost_unit_id' => $this->request->costUnit->id,
'type' => $this->request->estimateType,
'description' => $this->request->description,
], $amount));
} else {
$estimate = CostUnitEstimate::find($this->request->estimateId);
$estimate->update(array_merge([
'tenant' => app('tenant')->slug,
'cost_unit_id' => $this->request->costUnit->id,
'type' => $this->request->estimateType,
'description' => $this->request->description,
], $amount));
}
if ($estimate !== null) {
$this->response->estimateId = $estimate->id;
$this->response->success = true;
}
return $this->response;
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Domains\Budget\Actions\CreateEstimate;
use App\Enumerations\InvoiceType;
use App\Models\CostUnit;
use App\ValueObjects\Amount;
class CreateEstimateRequest {
function __construct(
public string $amountType,
public string $description,
public Amount $amount,
public string $estimateType,
public CostUnit $costUnit,
public int $estimateId,
) {
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Domains\Budget\Actions\CreateEstimate;
class CreateEstimateResponse {
public bool $success;
public ?int $estimateId;
public function __construct() {
$this->success = false;
$this->estimateId = null;
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Budget\Actions\DeleteEstimate;
class DeleteEstimateAction {
public function __construct(private DeleteEstimateRequest $request) {
}
public function execute() : DeleteEstimateResponse {
$response = new DeleteEstimateResponse();
$this->request->estimate->delete();
$response->success = true;
return $response;
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Budget\Actions\DeleteEstimate;
use App\Models\CostUnitEstimate;
class DeleteEstimateRequest {
public function __construct(public CostUnitEstimate $estimate)
{
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Budget\Actions\DeleteEstimate;
class DeleteEstimateResponse {
public bool $success;
public function __construct()
{
$this->success = false;
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Domains\Budget\Controllers;
use App\Domains\Budget\Actions\CreateEstimate\CreateEstimateAction;
use App\Domains\Budget\Actions\CreateEstimate\CreateEstimateRequest;
use App\Domains\Budget\Actions\DeleteEstimate\DeleteEstimateAction;
use App\Domains\Budget\Actions\DeleteEstimate\DeleteEstimateRequest;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DeleteController extends CommonController
{
public function __invoke(int $costUnitId, int $estimateId, Request $request) : JsonResponse {
$estimate = $this->estimates->getById($estimateId);
if ($estimate === null) {
return response()->json([
'status' => 'error',
'message' => 'Estimate not found'
], 404);
}
$deleteEstimateResponse =
new DeleteEstimateAction(request: new DeleteEstimateRequest($estimate)
)->execute();
if ($deleteEstimateResponse->success) {
return response()->json([
'status' => 'success',
'message' => 'Der Eintrag wurde erfolgreich gelöscht.'
]);
} else {
return response()->json([
'status' => 'error',
'message' => 'Beim Löschen des Eintrags ist ein Fehler aufgetreten.'
]);
}
}
}
@@ -17,7 +17,9 @@ class ListController extends CommonController
'status' => 'success',
'costUnitId' => $costUnitId,
'title' => InvoiceType::where('slug', $estimateType)->first()->name,
'estimateType' => $estimateType,
'estimates' => $estimates,
'totalAmountString' => $this->estimates->getTotalAmount($costUnit, $estimateType)->toString(),
]);
}
@@ -0,0 +1,47 @@
<?php
namespace App\Domains\Budget\Controllers;
use App\Domains\Budget\Actions\CreateEstimate\CreateEstimateAction;
use App\Domains\Budget\Actions\CreateEstimate\CreateEstimateRequest;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SaveController extends CommonController
{
public function __invoke(int $costUnitId, Request $request) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
if ($costUnit === null) {
return response()->json([
'status' => 'error',
'message' => 'Cost unit not found'
], 404);
}
$createCostUniResponse =
new CreateEstimateAction(request: new CreateEstimateRequest(
description: $request->input('description'),
amount: Amount::fromString($request->input('amount')),
amountType: $request->input('amount_type'),
estimateType: $request->input('estimateType'),
costUnit: $costUnit,
estimateId: $request->input('estimateId'),
))->execute();
if ($createCostUniResponse->success) {
return response()->json([
'status' => 'success',
'message' => 'Der Eintrag wurde erfolgreich angelegt.'
]);
} else {
return response()->json([
'status' => 'error',
'message' => 'Beim Anlegen des Eintrags ist ein Fehler aufgetreten.'
]);
}
}
}
+4
View File
@@ -1,5 +1,7 @@
<?php
use App\Domains\Budget\Controllers\DeleteController;
use App\Domains\Budget\Controllers\SaveController;
use App\Domains\Budget\Controllers\ListController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
@@ -10,6 +12,8 @@ Route::prefix('api/v1')->group(function () {
Route::middleware(['auth'])->group(function () {
Route::prefix('/{costUnitId}')->group(function () {
Route::get('/list/{estimateType}', ListController::class);
Route::get('{estimateId}/delete', DeleteController::class);
Route::post('/save-estimate', SaveController::class);
});
});
});
@@ -0,0 +1,100 @@
<script setup>
import Modal from "../../../Views/Components/Modal.vue";
import {reactive, ref} from "vue";
import AmountInput from "../../../Views/Components/AmountInput.vue";
import {toast} from "vue3-toastify";
import {useAjax} from "../../../../resources/js/components/ajaxHandler.js";
const { request } = useAjax()
const props = defineProps({
showAddEstimate: Boolean,
type: String,
title: String,
costUnitId: Number,
amount: Number,
amount_type: String,
estimateId: Number,
description: String,
})
console.log(props)
const form = reactive({
amount_type: props.amount_type,
amount: props.amount,
description: props.description,
})
async function save() {
const data = await request('/api/v1/budget/' + props.costUnitId + '/save-estimate', {
method: "POST",
body: {
estimateId: props.estimateId,
amount_type: form.amount_type,
amount: form.amount,
description: form.description,
estimateType: props.type,
}
});
if (data.status === 'success') {
toast.success(data.message);
} else {
toast.error(data.message);
}
emit('closeAddEstimate')
}
const emit = defineEmits(['closeAddEstimate'])
</script>
<template>
<Modal
:show="showAddEstimate"
@close="emit('closeAddEstimate')"
title="Ausgabenschätzung hinzufügen"
width="600px"
>
<table>
<tr>
<th>Kostenstelle</th>
<td>{{title}}</td>
</tr>
<tr>
<th>Verwendungszweck</th>
<td><input type="text" v-model="form.description" style="width: 250px;" /></td>
</tr>
<tr>
<th>Betrag</th>
<td><AmountInput v-model="form.amount" style="width: 100px;" /> Euro</td>
</tr>
<tr>
<th>Kostentyp</th>
<td style="vertical-align: top;">
<input type="radio" v-model="form.amount_type" value="flat"
id="amount_type_flat" />
<label for="amount_type_flat">Pauschal</label><br />
<input type="radio" v-model="form.amount_type" value="per_person" id="amount_type_per_person" />
<label for="amount_type_per_person">Pro Person</label><br />
</td>
</tr>
<tr>
<td colspan="2">
<input type="button" value="Speichern" class="button" @click="save" />
</td>
</tr>
</table>
</Modal>
</template>
<style scoped>
</style>
+62 -136
View File
@@ -3,189 +3,115 @@ import {createApp, ref} from 'vue'
import LoadingModal from "../../../Views/Components/LoadingModal.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import {toast} from "vue3-toastify";
import AddOrUpdateEstimate from "./AddOrUpdateEstimate.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 localData = ref(props.data)
const show_cost_unit = ref(false)
const showTreasurers = ref(false)
const costUnit = ref(null)
const showAddEstimate = ref(false)
const estimateId = ref(null)
const description = ref(null)
const amount = ref(null)
const amountType = ref(null)
const { data, loading, error, request, download } = useAjax()
async function costUnitDetails(costUnitId) {
const data = await request('/api/v1/cost-unit/' + costUnitId + '/details', {
async function reload() {
const url = "/api/v1/budget/" + props.data.costUnitId + "/list/" + props.data.estimateType
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 estimates:', err)
}
}
async function openAddEstimate() {
estimateId.value = 0
amount.value = 0.00
amountType.value = 'flat'
description.value = ''
showAddEstimate.value = true
}
async function openEditEstimate(localEstimateId, localDescription, localAmount, localAmountType, localEstimateType) {
estimateId.value = localEstimateId
description.value = localDescription
amount.value = localAmount
amountType.value = localAmountType
console.log(localEstimateId, localDescription, localAmount, localAmountType, localEstimateType)
console.log(estimateId.value, description.value, amount.value, amountType.value, localEstimateType)
showAddEstimate.value = true
}
async function deleteEstimate(currentEstimateId) {
const data = await request('/api/v1/budget/' + props.data.costUnitId + '/' + currentEstimateId + '/delete', {
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';
reload()
} else {
toast.error(data.message);
}
}
async function exportPayouts(costUnitId) {
showLoading.value = true;
const response = await fetch('/api/v1/core/retrieve-global-data');
const data = await response.json();
const exportUrl = '/api/v1/cost-unit/' + costUnitId + '/export-payouts';
try {
if (data.tenant.download_exports) {
const response = await fetch(exportUrl, {
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error('Fehler beim Export (ZIP)');
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = downloadUrl;
a.download = "Abrechnungen-Sippenstunden.zip";
document.body.appendChild(a);
a.click();
setTimeout(() => {
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
}, 100);
} else {
const response = await request(exportUrl, {
method: "GET",
});
toast.success(response.message);
}
showLoading.value = false;
} catch (err) {
showLoading.value = false;
toast.error('Beim Export der Abrechnungen ist ein Fehler aufgetreten.');
}
}
</script>
<template>
<div v-if="props.data.estimates && props.data.estimates.length > 0">
<div v-if="localData.estimates && localData.estimates.length > 0">
<h2>{{ props.data.title }}</h2>
<span v-for="estimate in props.data.estimates">
<table style="width: 100%">
<h3>Gesamtkosten: {{ localData.totalAmountString }}</h3>
<span v-for="estimate in localData.estimates">
<table style="width: 100%;">
<tr><th style="width: 200px;">
{{ estimate.title }}
</th>
<td>{{ estimate.totalAmountString }}</td>
<td>{{ estimate.singleAmountString }}</td>
</tr>
<tr>
<td></td>
<td>
Bearbeiten
Löschen
<td style="padding-bottom: 30px">
<label class="link" style="font-size: 10pt; margin-right: 20px;" @click="openEditEstimate(estimate.id, estimate.title, estimate.amountValue, estimate.amountType, props.data.estimateType)">Bearbeiten</label>
<label class="link" style="font-size: 10pt; margin-right: 20px; color: #ff0000;" @click="deleteEstimate(estimate.id)">Löschen</label>
</td>
</tr>
</table>
</span>
<CostUnitDetails :data="costUnit" :showCostUnit="show_cost_unit" v-if="show_cost_unit" @close="show_cost_unit = false" />
<Treasurers :data="costUnit" :showTreasurers="showTreasurers" v-if="showTreasurers" @closeTreasurers="showTreasurers = false" />
</div>
<div v-else-if="showInvoiceList">
<invoices :data="invoices" :load_invoice_id="props.deep_jump_id_sub" :cost_unit_id="current_cost_unit" />
</div>
<div v-else>
<strong style="width: 100%; text-align: center; display: block; margin-top: 20px;">
Noch keine geschätzten Ausgaben vorhanden
</strong>
</div>
<label class="link">
<label class="link" @click="openAddEstimate()">
Hinzufügen
</label>
<LoadingModal :show="showLoading" />
<AddOrUpdateEstimate
:amount="amount"
:amount_type="amountType"
:description="description"
:estimateId="estimateId"
:costUnitId="props.data.costUnitId"
:title="props.data.title"
:type="props.data.estimateType"
:showAddEstimate="showAddEstimate"
v-if="showAddEstimate"
@closeAddEstimate="showAddEstimate = false; reload()" />
</template>
<style scoped>
@@ -13,7 +13,6 @@
const invoice = ref(null)
const show_invoice = ref(false)
const localData = ref(props.data)
console.log(props.data)
async function openInvoiceDetails(invoiceId) {
const url = '/api/v1/invoice/details/' + invoiceId
+2 -4
View File
@@ -42,12 +42,10 @@ class CostUnitEstimate extends InstancedModel
default:
$event = $this->costUnit()->first()->event()?->first();
if (null !== $event) {
$participants = $event->participants()->count();
return $this->amount_by_user->multiply($participants);
$amount = clone($this->amount_by_user);
return $amount->multiply($participants);
} else {
dd('U');
return $this->amount_by_user;
}
}
+28
View File
@@ -3,6 +3,8 @@
namespace App\Repositories;
use App\Models\CostUnit;
use App\Models\CostUnitEstimate;
use App\ValueObjects\Amount;
class EstimatesRepository {
public function getEstimates(CostUnit $costUnit, string $estimateType) : array {
@@ -13,4 +15,30 @@ class EstimatesRepository {
return $return;
}
public function getById(int $estimateId, bool $accessCheck = true) : ?CostUnitEstimate {
$estimate = CostUnitEstimate::find($estimateId);
if ($estimate === null) {
return null;
}
if ($accessCheck) {
$costUnitRepository = new CostUnitRepository();
if (null === $costUnitRepository->getById($estimate->cost_unit_id)) {
return null;
}
}
return $estimate;
}
public function getTotalAmount(CostUnit $costUnit, string $estimateType) : Amount {
$total = new Amount(0, 'Euro');
foreach ($costUnit->estimates()->where('type', $estimateType)->get() as $estimate) {
$total->addAmount($estimate->calculateAmount());
}
return $total;
}
}
+15 -5
View File
@@ -8,6 +8,7 @@ use App\Enumerations\ParticipationType;
use App\Models\CostUnitEstimate;
use App\Models\EventParticipant;
use App\ValueObjects\Age;
use App\ValueObjects\Amount;
use Illuminate\Http\Resources\Json\JsonResource;
class CostUnitEstimateResource extends JsonResource
@@ -19,17 +20,26 @@ class CostUnitEstimateResource extends JsonResource
public function toArray($request) : array
{
$amountString = $this->resource->flat_amount?->toString();
if ($amountString === null) {
$amountString = $this->resource->amount_by_user?->toString() . ' / Person';
$amount = $this->resource->calculateAmount();
$singleAmountString = $this->resource->flat_amount?->toString();
$amountType = 'flat';
if ($singleAmountString === null) {
$amountType = 'per_person';
$singleAmountString = $this->resource->amount_by_user->toString() . ' / Person (' . $amount->toString() . ' Gesamt)';
} else {
$amountString .= ' Gesamt';
$singleAmountString .= ' Gesamt';
}
return [
'id' => $this->resource->id,
'title' => $this->resource->description,
'totalAmountString' => $amountString,
'singleAmountString' => $singleAmountString,
'calculatedAmount' => $amount,
'calculatedAmountString' => $amount->toString(),
'amountValue' => $amount->getAmount(),
'amountType' => $amountType,
];
}
}
+1 -1
View File
@@ -1 +1 @@
4.3.1
4.4.0