diff --git a/app/Domains/Budget/Actions/CreateEstimate/CreateEstimateAction.php b/app/Domains/Budget/Actions/CreateEstimate/CreateEstimateAction.php
new file mode 100644
index 0000000..0f0cf21
--- /dev/null
+++ b/app/Domains/Budget/Actions/CreateEstimate/CreateEstimateAction.php
@@ -0,0 +1,50 @@
+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;
+ }
+}
diff --git a/app/Domains/Budget/Actions/CreateEstimate/CreateEstimateRequest.php b/app/Domains/Budget/Actions/CreateEstimate/CreateEstimateRequest.php
new file mode 100644
index 0000000..c34d618
--- /dev/null
+++ b/app/Domains/Budget/Actions/CreateEstimate/CreateEstimateRequest.php
@@ -0,0 +1,19 @@
+success = false;
+ $this->estimateId = null;
+ }
+}
diff --git a/app/Domains/Budget/Actions/DeleteEstimate/DeleteEstimateAction.php b/app/Domains/Budget/Actions/DeleteEstimate/DeleteEstimateAction.php
new file mode 100644
index 0000000..0f8e87e
--- /dev/null
+++ b/app/Domains/Budget/Actions/DeleteEstimate/DeleteEstimateAction.php
@@ -0,0 +1,16 @@
+request->estimate->delete();
+ $response->success = true;
+ return $response;
+ }
+}
diff --git a/app/Domains/Budget/Actions/DeleteEstimate/DeleteEstimateRequest.php b/app/Domains/Budget/Actions/DeleteEstimate/DeleteEstimateRequest.php
new file mode 100644
index 0000000..85a6476
--- /dev/null
+++ b/app/Domains/Budget/Actions/DeleteEstimate/DeleteEstimateRequest.php
@@ -0,0 +1,12 @@
+success = false;
+ }
+}
diff --git a/app/Domains/Budget/Controllers/DeleteController.php b/app/Domains/Budget/Controllers/DeleteController.php
new file mode 100644
index 0000000..612f7de
--- /dev/null
+++ b/app/Domains/Budget/Controllers/DeleteController.php
@@ -0,0 +1,43 @@
+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.'
+ ]);
+ }
+ }
+}
diff --git a/app/Domains/Budget/Controllers/ListController.php b/app/Domains/Budget/Controllers/ListController.php
new file mode 100644
index 0000000..e93602b
--- /dev/null
+++ b/app/Domains/Budget/Controllers/ListController.php
@@ -0,0 +1,26 @@
+costUnits->getById($costUnitId);
+ $estimates = $this->estimates->getEstimates($costUnit, $estimateType);
+
+ return response()->json([
+ 'status' => 'success',
+ 'costUnitId' => $costUnitId,
+ 'title' => InvoiceType::where('slug', $estimateType)->first()->name,
+ 'estimateType' => $estimateType,
+ 'estimates' => $estimates,
+ 'totalAmountString' => $this->estimates->getTotalAmount($costUnit, $estimateType)->toString(),
+ ]);
+ }
+
+}
diff --git a/app/Domains/Budget/Controllers/MainController.php b/app/Domains/Budget/Controllers/MainController.php
new file mode 100644
index 0000000..0fb6ff9
--- /dev/null
+++ b/app/Domains/Budget/Controllers/MainController.php
@@ -0,0 +1,19 @@
+ $costUnitId
+ ]);
+ return $inertiaProvider->render();
+ }
+}
diff --git a/app/Domains/Budget/Controllers/SaveController.php b/app/Domains/Budget/Controllers/SaveController.php
new file mode 100644
index 0000000..72c4e1b
--- /dev/null
+++ b/app/Domains/Budget/Controllers/SaveController.php
@@ -0,0 +1,47 @@
+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.'
+ ]);
+ }
+ }
+}
diff --git a/app/Domains/Budget/Routes/api.php b/app/Domains/Budget/Routes/api.php
new file mode 100644
index 0000000..d01a5ee
--- /dev/null
+++ b/app/Domains/Budget/Routes/api.php
@@ -0,0 +1,21 @@
+group(function () {
+ Route::middleware(IdentifyTenant::class)->group(function () {
+ Route::prefix('budget')->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);
+ });
+ });
+ });
+ });
+});
diff --git a/app/Domains/Budget/Routes/web.php b/app/Domains/Budget/Routes/web.php
new file mode 100644
index 0000000..e96b0ef
--- /dev/null
+++ b/app/Domains/Budget/Routes/web.php
@@ -0,0 +1,20 @@
+group(function () {
+ Route::prefix('budget')->group(function () {
+ Route::middleware(['auth'])->group(function () {
+ Route::prefix('/{costUnitId}')->group(function() {
+ Route::get('/', MainController::class);
+ });
+ });
+ });
+});
+
+
+
+
+
diff --git a/app/Domains/Budget/Views/AddOrUpdateEstimate.vue b/app/Domains/Budget/Views/AddOrUpdateEstimate.vue
new file mode 100644
index 0000000..16cfc5a
--- /dev/null
+++ b/app/Domains/Budget/Views/AddOrUpdateEstimate.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/Domains/Budget/Views/List.vue b/app/Domains/Budget/Views/List.vue
new file mode 100644
index 0000000..facf885
--- /dev/null
+++ b/app/Domains/Budget/Views/List.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Domains/Budget/Views/ListBudgetTypes.vue b/app/Domains/Budget/Views/ListBudgetTypes.vue
new file mode 100644
index 0000000..7073d92
--- /dev/null
+++ b/app/Domains/Budget/Views/ListBudgetTypes.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
{{ props.data.title }}
+
Gesamtkosten: {{ localData.totalAmountString }}
+
+
+ |
+ {{ estimate.title }}
+ |
+ {{ estimate.singleAmountString }} |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+ Noch keine geschätzten Ausgaben vorhanden
+
+
+
+
+
+
+
+
diff --git a/app/Domains/CostUnit/Views/Partials/ListInvoices.vue b/app/Domains/CostUnit/Views/Partials/ListInvoices.vue
index efc647e..2673fad 100644
--- a/app/Domains/CostUnit/Views/Partials/ListInvoices.vue
+++ b/app/Domains/CostUnit/Views/Partials/ListInvoices.vue
@@ -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
diff --git a/app/Domains/Event/Views/Partials/Overview.vue b/app/Domains/Event/Views/Partials/Overview.vue
index 0e28bda..920e544 100644
--- a/app/Domains/Event/Views/Partials/Overview.vue
+++ b/app/Domains/Event/Views/Partials/Overview.vue
@@ -112,8 +112,8 @@
-
-
+
+
@@ -248,13 +249,13 @@
gap: 10px; /* Abstand zwischen den Spalten */
}
-.event-flexbox-row.top .left {
+.event-flexbox-row.top .actions-left {
flex: 0 0 calc(100% - 300px);
padding: 10px;
}
-.event-flexbox-row.top .right {
- flex: 0 0 250px;
+.event-flexbox-row.top .actions-right {
+ flex: 0 0 200px;
padding: 10px;
}
@@ -263,7 +264,7 @@
padding: 10px;
}
-.event-flexbox-row.top .right input[type="button"] {
+.event-flexbox-row.top .actions-right input[type="button"] {
width: 100% !important;
margin-bottom: 10px;
}
diff --git a/app/Domains/Event/Views/Partials/ParticipationSummary.vue b/app/Domains/Event/Views/Partials/ParticipationSummary.vue
index 3394bd6..3c605bd 100644
--- a/app/Domains/Event/Views/Partials/ParticipationSummary.vue
+++ b/app/Domains/Event/Views/Partials/ParticipationSummary.vue
@@ -96,19 +96,31 @@ const props = defineProps({
+
+ | Budget |
+
+ {{ props.event.totalBalance.estimated.readable }}
+ |
+
+ {{props.event.totalBalance.estimated.readable}}
+ |
+
+
Ausgaben
-
+
| {{amount.name}} |
{{amount.string}} |
+ ({{ amount.estimatedString }}) |
| Gesamt |
- {{props.event.costUnit.overAllAmount.text}} |
+ {{props.event.costUnit.overAllAmount.text}} |
+ ({{props.event.costUnit.overAllEstimatedAmount.text}})) |
@@ -121,7 +133,7 @@ const props = defineProps({
.participant-flexbox {
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: 20px;
width: 95%;
margin: 20px auto 0;
}
@@ -135,7 +147,7 @@ const props = defineProps({
.participant-flexbox-row.top .left,
.participant-flexbox-row.top .right {
- padding: 10px;
+ padding: 20px;
min-width: 0;
}
diff --git a/app/Models/CostUnit.php b/app/Models/CostUnit.php
index afb66e3..8f82621 100644
--- a/app/Models/CostUnit.php
+++ b/app/Models/CostUnit.php
@@ -6,6 +6,7 @@ use App\Scopes\InstancedModel;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* @property string $name
@@ -44,7 +45,15 @@ class CostUnit extends InstancedModel
return $this->hasMany(Invoice::class);
}
+ public function estimates() : hasMany {
+ return $this->hasMany(CostUnitEstimate::class);
+ }
+
public function tenant() : BelongsTo {
return $this->belongsTo(Tenant::class, 'tenant', 'slug');
}
+
+ public function event() : HasOne {
+ return $this->hasOne(Event::class);
+ }
}
diff --git a/app/Models/CostUnitEstimate.php b/app/Models/CostUnitEstimate.php
new file mode 100644
index 0000000..c5fcd46
--- /dev/null
+++ b/app/Models/CostUnitEstimate.php
@@ -0,0 +1,53 @@
+ AmountCast::class,
+ 'amount_by_user' => AmountCast::class,
+ ];
+
+ public function costUnit() : BelongsTo{
+ return $this->belongsTo(CostUnit::class);
+ }
+
+ public function invoiceType() : InvoiceType {
+ return $this->belongsTo(InvoiceType::class, 'type', 'slug')->first();
+ }
+
+ public function calculateAmount() : ?Amount {
+ switch (true) {
+ case $this->flat_amount !== null:
+ return $this->flat_amount;
+ default:
+ $event = $this->costUnit()->first()->event()?->first();
+ if (null !== $event) {
+ $participants = $event->participants()->count();
+ $amount = clone($this->amount_by_user);
+ return $amount->multiply($participants);
+ } else {
+ return $this->amount_by_user;
+ }
+ }
+ }
+}
diff --git a/app/Repositories/CostUnitRepository.php b/app/Repositories/CostUnitRepository.php
index 75df613..a976501 100644
--- a/app/Repositories/CostUnitRepository.php
+++ b/app/Repositories/CostUnitRepository.php
@@ -178,6 +178,18 @@ class CostUnitRepository {
return $amount;
}
+ public function sumupEstimatedByInvoiceType(CostUnit $costUnit, InvoiceType $invoiceType) : Amount {
+ $amount = new Amount(0, 'Euro');
+ foreach ($costUnit->estimates()->get() as $estimate) {
+ if ($estimate->type !== $invoiceType->slug) {
+ continue;
+ }
+
+ $amount->addAmount($estimate->calculateAmount());
+ }
+ return $amount;
+ }
+
public function sumupUnhandledAmounts(CostUnit $costUnit, bool $donatedAmount = false) : Amount {
$amount = new Amount(0, '');
diff --git a/app/Repositories/EstimatesRepository.php b/app/Repositories/EstimatesRepository.php
new file mode 100644
index 0000000..3b6dc80
--- /dev/null
+++ b/app/Repositories/EstimatesRepository.php
@@ -0,0 +1,44 @@
+estimates()->where('type', $estimateType)->get() as $estimate) {
+ $return[] = $estimate->toResource()->toArray(request());
+ }
+
+ 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;
+ }
+
+}
diff --git a/app/Resources/CostUnitEstimateResource.php b/app/Resources/CostUnitEstimateResource.php
new file mode 100644
index 0000000..eaa2a15
--- /dev/null
+++ b/app/Resources/CostUnitEstimateResource.php
@@ -0,0 +1,45 @@
+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 {
+ $singleAmountString .= ' Gesamt';
+ }
+
+ return [
+ 'id' => $this->resource->id,
+ 'title' => $this->resource->description,
+ 'singleAmountString' => $singleAmountString,
+ 'calculatedAmount' => $amount,
+ 'calculatedAmountString' => $amount->toString(),
+ 'amountValue' => $amount->getAmount(),
+ 'amountType' => $amountType,
+
+
+ ];
+ }
+}
diff --git a/app/Resources/CostUnitResource.php b/app/Resources/CostUnitResource.php
index 1799798..15d156c 100644
--- a/app/Resources/CostUnitResource.php
+++ b/app/Resources/CostUnitResource.php
@@ -31,10 +31,15 @@ class CostUnitResource {
$amounts = [];
$overAllAmount = new Amount(0, 'Euro');
+ $overAllEstimatedAmount = new Amount(0, 'Euro');
foreach (InvoiceType::orderBy('sort_order')->get() as $invoiceType) {
$overAllAmount->addAmount($costUnitRepository->sumupByInvoiceType($this->costUnit, $invoiceType));
+ $overAllEstimatedAmount->addAmount($costUnitRepository->sumupEstimatedByInvoiceType($this->costUnit, $invoiceType));
$amounts[$invoiceType->slug]['string'] = $costUnitRepository->sumupByInvoiceType($this->costUnit, $invoiceType)->toString();
$amounts[$invoiceType->slug]['name'] = $invoiceType->name;
+ $amounts[$invoiceType->slug]['estimated'] = $costUnitRepository->sumupEstimatedByInvoiceType($this->costUnit, $invoiceType);
+ $amounts[$invoiceType->slug]['estimatedString'] = $costUnitRepository->sumupEstimatedByInvoiceType($this->costUnit, $invoiceType)->toString();
+
}
@@ -52,6 +57,7 @@ class CostUnitResource {
'treasurers' => $this->costUnit->treasurers()->get()->map(fn($user) => new UserResource($user))->toArray(),
'amounts' => $amounts,
'overAllAmount' => ['text' => $overAllAmount->toString(), 'value' => $overAllAmount],
+ 'overAllEstimatedAmount' => ['text' => $overAllEstimatedAmount->toString(), 'value' => $overAllEstimatedAmount],
]);
diff --git a/app/Resources/EventResource.php b/app/Resources/EventResource.php
index 14bb4cf..ca7428a 100644
--- a/app/Resources/EventResource.php
+++ b/app/Resources/EventResource.php
@@ -86,6 +86,7 @@ class EventResource extends JsonResource{
$returnArray['eventEnd'] = $this->event->end_date->format('d.m.Y');
$returnArray['eventEndInternal'] = $this->event->end_date;
$returnArray['duration'] = $duration;
+ $returnArray['totalParticipantCount'] = $this->event->participants()->count();
$returnArray['supportPersonIndex'] = $this->event->support_per_person->toString();
$returnArray['supportPerson'] = $this->calculateSupportPerPerson($returnArray['participants']);
@@ -95,12 +96,15 @@ class EventResource extends JsonResource{
$totalBalanceReal = new Amount(0, 'Euro');
$totalBalanceExpected = new Amount(0, 'Euro');
+ $totalBalanceEstimated = new Amount(0, 'Euro');
$totalBalanceReal->addAmount($returnArray['income']['real']['amount']);
$totalBalanceExpected->addAmount($returnArray['income']['expected']['amount']);
+ $totalBalanceEstimated->addAmount($returnArray['income']['expected']['amount']);
$totalBalanceReal->subtractAmount($returnArray['costUnit']['overAllAmount']['value']);
$totalBalanceExpected->subtractAmount($returnArray['costUnit']['overAllAmount']['value']);
+ $totalBalanceEstimated->subtractAmount($returnArray['costUnit']['overAllEstimatedAmount']['value']);
$returnArray['totalBalance'] = [
'real' => [
'value' => $totalBalanceReal->getAmount(),
@@ -108,7 +112,11 @@ class EventResource extends JsonResource{
], 'expected' => [
'value' => $totalBalanceExpected->getAmount(),
'readable' => $totalBalanceExpected->toString(),
- ]
+ ],
+ 'estimated' => [
+ 'value' => $totalBalanceEstimated->getAmount(),
+ 'readable' => $totalBalanceEstimated->toString(),
+ ]
];
$returnArray['flatSupport'] = $this->event->support_flat->toString();
diff --git a/app/Scopes/CommonController.php b/app/Scopes/CommonController.php
index a1bb05d..dd0efae 100644
--- a/app/Scopes/CommonController.php
+++ b/app/Scopes/CommonController.php
@@ -5,6 +5,7 @@ namespace App\Scopes;
use App\Models\Tenant;
use App\Providers\AuthCheckProvider;
use App\Repositories\CostUnitRepository;
+use App\Repositories\EstimatesRepository;
use App\Repositories\EventParticipantRepository;
use App\Repositories\EventRepository;
use App\Repositories\InvoiceRepository;
@@ -21,6 +22,7 @@ abstract class CommonController {
protected InvoiceRepository $invoices;
protected EventRepository $events;
protected EventParticipantRepository $eventParticipants;
+ protected EstimatesRepository $estimates;
public function __construct() {
$this->tenant = app('tenant');
@@ -30,6 +32,7 @@ abstract class CommonController {
$this->invoices = new InvoiceRepository();
$this->events = new EventRepository();
$this->eventParticipants = new EventParticipantRepository();
+ $this->estimates = new EstimatesRepository();
}
protected function checkAuth() {
diff --git a/database/migrations/2026_05_25_140010_create_cost_unit_estimates.php b/database/migrations/2026_05_25_140010_create_cost_unit_estimates.php
new file mode 100644
index 0000000..3c0aee3
--- /dev/null
+++ b/database/migrations/2026_05_25_140010_create_cost_unit_estimates.php
@@ -0,0 +1,32 @@
+id();
+ $table->string('tenant');
+ $table->foreignId('cost_unit_id')->constrained('cost_units', 'id')->restrictOnDelete()->cascadeOnUpdate();
+ $table->string('type');
+ $table->string('description');
+ $table->float('flat_amount', 2)->nullable();
+ $table->float('amount_by_user', 2)->nullable();
+
+ $table->foreign('tenant')->references('slug')->on('tenants')->restrictOnDelete()->cascadeOnUpdate();
+ $table->foreign('type')->references('slug')->on('invoice_types')->restrictOnDelete()->cascadeOnUpdate();
+
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('cost_unit_estimates');
+ }
+};
diff --git a/routes/web.php b/routes/web.php
index bbbd546..e521279 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -21,6 +21,8 @@ require_once __DIR__ . '/../app/Domains/Invoice/Routes/web.php';
require_once __DIR__ . '/../app/Domains/Invoice/Routes/api.php';
require_once __DIR__ . '/../app/Domains/Event/Routes/web.php';
require_once __DIR__ . '/../app/Domains/Event/Routes/api.php';
+require_once __DIR__ . '/../app/Domains/Budget/Routes/web.php';
+require_once __DIR__ . '/../app/Domains/Budget/Routes/api.php';
Route::get('/LKvDUqWl', function () {
diff --git a/version b/version
index f77856a..fdc6698 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-4.3.1
+4.4.0