Displaying estimates

This commit is contained in:
2026-05-26 11:07:59 +02:00
parent fe3429cd4e
commit 575fb27018
10 changed files with 506 additions and 0 deletions
@@ -0,0 +1,24 @@
<?php
namespace App\Domains\Budget\Controllers;
use App\Enumerations\InvoiceType;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListController extends CommonController
{
public function __invoke(int $costUnitId, string $estimateType, Request $request) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
$estimates = $this->estimates->getEstimates($costUnit, $estimateType);
return response()->json([
'status' => 'success',
'costUnitId' => $costUnitId,
'title' => InvoiceType::where('slug', $estimateType)->first()->name,
'estimates' => $estimates,
]);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Domains\Budget\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Inertia\Response;
class MainController extends CommonController
{
public function __invoke(int $costUnitId, Request $request) : Response
{
$inertiaProvider = new InertiaProvider('Budget/List', [
'cost_unit_id' => $costUnitId
]);
return $inertiaProvider->render();
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
use App\Domains\Budget\Controllers\ListController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::prefix('api/v1')->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);
});
});
});
});
});
+20
View File
@@ -0,0 +1,20 @@
<?php
use App\Domains\Budget\Controllers\MainController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('budget')->group(function () {
Route::middleware(['auth'])->group(function () {
Route::prefix('/{costUnitId}')->group(function() {
Route::get('/', MainController::class);
});
});
});
});
+93
View File
@@ -0,0 +1,93 @@
<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 ListBudgets from "./ListBudgetTypes.vue";
const props = defineProps({
message: String,
data: {
type: [Array, Object],
default: () => []
},
cost_unit_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 tabs = [
{
title: 'Verpflegung',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/catering",
deep_jump_id: initialCostUnitId,
},
{
title: 'Unterkunft',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/accommodation",
deep_jump_id: initialCostUnitId,
},
{
title: 'Programm',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/program",
deep_jump_id: initialCostUnitId,
},
{
title: 'Logistik',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/logistic",
deep_jump_id: initialCostUnitId,
},
{
title: 'Technik',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/technical",
deep_jump_id: initialCostUnitId,
},
{
title: 'Reisekosten',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/travelling",
deep_jump_id: initialCostUnitId,
},
{
title: 'Verwaltung',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/management",
deep_jump_id: initialCostUnitId,
},
{
title: 'Sonstiges',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/other",
deep_jump_id: initialCostUnitId,
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<AppLayout title="Veranstaltungsbudget">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" :initial-tab-id="initialCostUnitId" />
</shadowed-box>
</AppLayout>
</template>
@@ -0,0 +1,195 @@
<script setup>
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";
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()
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 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">
<h2>{{ props.data.title }}</h2>
<span v-for="estimate in props.data.estimates">
<table style="width: 100%">
<tr><th style="width: 200px;">
{{ estimate.title }}
</th>
<td>{{ estimate.totalAmountString }}</td>
</tr>
<tr>
<td></td>
<td>
Bearbeiten
Löschen
</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">
Hinzufügen
</label>
<LoadingModal :show="showLoading" />
</template>
<style scoped>
.costunit-list {
width: 96% !important;
}
</style>
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use App\Casts\AmountCast;
use App\Enumerations\InvoiceStatus;
use App\Enumerations\InvoiceType;
use App\Resources\EventResource;
use App\Scopes\InstancedModel;
use App\ValueObjects\Amount;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CostUnitEstimate extends InstancedModel
{
protected $fillable = [
'tenant',
'cost_unit_id',
'type',
'description',
'flat_amount',
'amount_by_user',
];
protected $casts = [
'flat_amount' => 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();
return $this->amount_by_user->multiply($participants);
} else {
dd('U');
return $this->amount_by_user;
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Repositories;
use App\Models\CostUnit;
class EstimatesRepository {
public function getEstimates(CostUnit $costUnit, string $estimateType) : array {
$return = [];
foreach ($costUnit->estimates()->where('type', $estimateType)->get() as $estimate) {
$return[] = $estimate->toResource()->toArray(request());
}
return $return;
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Resources;
use App\Enumerations\EatingHabit;
use App\Enumerations\EfzStatus;
use App\Enumerations\ParticipationType;
use App\Models\CostUnitEstimate;
use App\Models\EventParticipant;
use App\ValueObjects\Age;
use Illuminate\Http\Resources\Json\JsonResource;
class CostUnitEstimateResource extends JsonResource
{
function __construct(CostUnitEstimate $estimate)
{
parent::__construct($estimate);
}
public function toArray($request) : array
{
$amountString = $this->resource->flat_amount?->toString();
if ($amountString === null) {
$amountString = $this->resource->amount_by_user?->toString() . ' / Person';
} else {
$amountString .= ' Gesamt';
}
return [
'id' => $this->resource->id,
'title' => $this->resource->description,
'totalAmountString' => $amountString,
];
}
}