Invoices can be uploaded
This commit is contained in:
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -23,12 +24,12 @@ class CreateController extends CommonController{
|
||||
$request->get('cost_unit_name'),
|
||||
CostUnitType::COST_UNIT_TYPE_RUNNING_JOB,
|
||||
Amount::fromString($request->get('distance_allowance')),
|
||||
$request->get('mail_on_new')
|
||||
$request->get('mailOnNew')
|
||||
);
|
||||
|
||||
$createCostUnitCommand = new CreateCostUnitCommand($createCostUnitRequest);
|
||||
$result = $createCostUnitCommand->execute();
|
||||
session()->put('message', 'Die laufende Tätigkeit wurde erfolgreich angelegt.');
|
||||
new FlashMessageProvider('Die laufende Tätigkeit wurde erfolgreich angelegt.', 'success');
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
<?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\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::get('/get-distance-allowance/{costUnitId}', DistanceAllowanceController::class);
|
||||
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/create-running-job', [CreateController::class, 'createCostUnitRunningJob']);
|
||||
|
||||
@@ -41,8 +41,6 @@ Route::prefix('api/v1')
|
||||
Route::get('/closed-cost-units', [ListController::class, 'listClosedCostUnits']);
|
||||
Route::get('/archived-cost-units', [ListController::class, 'listArchivedCostUnits']);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<?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 () {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<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>
|
||||
|
||||
@@ -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,
|
||||
'userId' => $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->path : 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
138
app/Domains/Invoice/Controllers/NewInvoiceController.php
Normal file
138
app/Domains/Invoice/Controllers/NewInvoiceController.php
Normal 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());
|
||||
}
|
||||
}
|
||||
25
app/Domains/Invoice/Routes/api.php
Normal file
25
app/Domains/Invoice/Routes/api.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\CostUnit\Controllers\DistanceAllowanceController;
|
||||
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('api/v1/invoice')->group(function () {
|
||||
Route::post('/new/{costUnitId}/{invoiceType}', [NewInvoiceController::class, 'saveInvoice']);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/create', [CreateController::class, 'showForm']);
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
22
app/Domains/Invoice/Routes/web.php
Normal file
22
app/Domains/Invoice/Routes/web.php
Normal 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']);
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
<script setup>
|
||||
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
|
||||
|
||||
const props = defineProps({
|
||||
navbar: Object,
|
||||
tenant: String,
|
||||
user: Object,
|
||||
currentPath: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title='Neue Abrechnung' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
|
||||
<!-- Alles hier wird in den Slot von AppLayout eingefügt -->
|
||||
<h2>Dashboard Content</h2>
|
||||
<p>Test 1!
|
||||
Hier wird mal eine Rechnung erstellt.
|
||||
Wenn es geht oder auch nicht</p>
|
||||
{{props.tenant}}
|
||||
|
||||
<button @click="$toast.success('Hallo vom Dashboard!')">Test Toaster</button>
|
||||
<button @click="$toast.error('Soe sieht ein Fehler aus')">Error Toaster</button>
|
||||
</AppLayout>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
79
app/Domains/Invoice/Views/NewInvoice.vue
Normal file
79
app/Domains/Invoice/Views/NewInvoice.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
157
app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue
Normal file
157
app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Domains\UserManagement\Controllers\LogOutController;
|
||||
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;
|
||||
|
||||
Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
@@ -23,11 +22,6 @@ Route::middleware(IdentifyTenant::class)->group(function () {
|
||||
Route::post('/logout', [LogoutController::class, 'logout']);
|
||||
|
||||
});
|
||||
|
||||
Route::prefix('api/v1/') ->group(function () {
|
||||
Route::get('/retreive-global-data', GlobalDataProvider::class);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
20
app/Enumerations/InvoiceStatus.php
Normal file
20
app/Enumerations/InvoiceStatus.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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_STAUTS_DELETED = 'deleted';
|
||||
|
||||
protected $table = 'invoice_status';
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
];
|
||||
}
|
||||
23
app/Enumerations/InvoiceType.php
Normal file
23
app/Enumerations/InvoiceType.php
Normal 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',
|
||||
];
|
||||
}
|
||||
@@ -5,6 +5,8 @@ namespace App\Installer;
|
||||
use App\Enumerations\CostUnitType;
|
||||
use App\Enumerations\EatingHabit;
|
||||
use App\Enumerations\FirstAidPermission;
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Enumerations\SwimmingPermission;
|
||||
use App\Enumerations\UserRole;
|
||||
use App\Models\Tenant;
|
||||
@@ -17,6 +19,8 @@ class ProductionDataSeeder {
|
||||
$this->installEatingHabits();
|
||||
$this->installFirstAidPermissions();
|
||||
$this->installTenants();
|
||||
$this->installInvoiceMetaData();
|
||||
|
||||
}
|
||||
|
||||
private function installUserRoles() {
|
||||
@@ -68,4 +72,37 @@ class ProductionDataSeeder {
|
||||
'has_active_instance' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private function installInvoiceMetaData() {
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_TRAVELLING,
|
||||
'name' => 'Reisekosten'
|
||||
]);
|
||||
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_PROGRAM,
|
||||
'name' => 'Programmkosten'
|
||||
]);
|
||||
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_ACCOMMODATION,
|
||||
'name' => 'Unterkunftskosten'
|
||||
]);
|
||||
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_CATERING,
|
||||
'name' => 'Verpflegungskosten',
|
||||
]);
|
||||
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_OTHER,
|
||||
'name' => 'Sonstige Kosten'
|
||||
]);
|
||||
|
||||
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_NEW]);
|
||||
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_APPROVED]);
|
||||
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_EXPORTED]);
|
||||
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_DENIED]);
|
||||
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STAUTS_DELETED]);
|
||||
}
|
||||
}
|
||||
|
||||
68
app/Models/Invoice.php
Normal file
68
app/Models/Invoice.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Scopes\InstancedModel;
|
||||
|
||||
/**
|
||||
* @property string $tenant
|
||||
* @property string $cost_unit_id
|
||||
* @property string $invoice_number
|
||||
* @property string $status
|
||||
* @property string $type
|
||||
* @property string $type_other
|
||||
* @property boolean $donation
|
||||
* @property string $user_id
|
||||
* @property string $contact_name
|
||||
* @property string $contact_email
|
||||
* @property string $contact_phone
|
||||
* @property string $contact_bank_owner
|
||||
* @property string $contact_bank_iban
|
||||
* @property float $amount
|
||||
* @property integer $distance
|
||||
* @property string $comment
|
||||
* @property string $changes
|
||||
* @property string $travel_direction
|
||||
* @property boolean $passengers
|
||||
* @property boolean $transportation
|
||||
* @property string $document_filename
|
||||
* @property string $approved_by
|
||||
* @property string $approved_at
|
||||
* @property boolean $upload_required
|
||||
* @property string $denied_by
|
||||
* @property string $denied_at
|
||||
* @property string $denied_reason
|
||||
*/
|
||||
class Invoice extends InstancedModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'tenant',
|
||||
'cost_unit_id',
|
||||
'invoice_number',
|
||||
'status',
|
||||
'type',
|
||||
'type_other',
|
||||
'donation',
|
||||
'user_id',
|
||||
'contact_name',
|
||||
'contact_email',
|
||||
'contact_phone',
|
||||
'contact_bank_owner',
|
||||
'contact_bank_iban',
|
||||
'amount',
|
||||
'donation',
|
||||
'distance',
|
||||
'comment',
|
||||
'changes',
|
||||
'travel_direction',
|
||||
'passengers',
|
||||
'transportation',
|
||||
'document_filename',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'upload_required',
|
||||
'denied_by',
|
||||
'denied_at',
|
||||
'denied_reason',
|
||||
];
|
||||
}
|
||||
9
app/Models/PageText.php
Normal file
9
app/Models/PageText.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Scopes\CommonModel;
|
||||
|
||||
class PageText extends CommonModel{
|
||||
protected $fillable = ['name', 'content'];
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $username
|
||||
* @property string $local_group
|
||||
* @property string $firstname
|
||||
@@ -26,6 +27,7 @@ use Illuminate\Notifications\Notifiable;
|
||||
* @property string $eating_habits
|
||||
* @property string $swimming_permission
|
||||
* @property string $first_aid_permission
|
||||
* @property string $bank_account_owner
|
||||
* @property string $bank_account_iban
|
||||
* @property string $password
|
||||
* @property boolean $active
|
||||
@@ -67,6 +69,7 @@ class User extends Authenticatable
|
||||
'eating_habits',
|
||||
'swimming_permission',
|
||||
'first_aid_permission',
|
||||
'$bank_account_owner',
|
||||
'bank_account_iban',
|
||||
'password',
|
||||
'active',
|
||||
@@ -93,12 +96,17 @@ class User extends Authenticatable
|
||||
];
|
||||
}
|
||||
|
||||
public function getOfficialName() : string {
|
||||
return sprintf('%1$s %2$s', $this->firstname, $this->lastname);
|
||||
}
|
||||
|
||||
public function getFullname() : string {
|
||||
return sprintf('%1$1s %2$s %3$s',
|
||||
$this->firstname,
|
||||
$this->lastname,
|
||||
$this->nickname !== null ? '(' . $this->nickname . ')' : '',
|
||||
$this->lastname
|
||||
);
|
||||
)
|
||||
|>trim(...);
|
||||
}
|
||||
|
||||
public function getNicename() : string {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Enumerations\UserRole;
|
||||
|
||||
class AuthCheckProvider {
|
||||
public function checkLoggedIn() : bool {
|
||||
if (!auth()->check()) {
|
||||
@@ -14,8 +16,11 @@ class AuthCheckProvider {
|
||||
return $user->active;
|
||||
}
|
||||
|
||||
if ($user->user_role_main === UserRole::USER_ROLE_ADMIN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->active && $tenant->slug === $user->tenant;
|
||||
return $user->active && $tenant->slug === $user->local_group;
|
||||
}
|
||||
|
||||
public function getUserRole() : ?string {
|
||||
|
||||
11
app/Providers/FlashMessageProvider.php
Normal file
11
app/Providers/FlashMessageProvider.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
class FlashMessageProvider {
|
||||
public function __construct(string $message, string $messageType) {
|
||||
session()->put('message',
|
||||
serialize(['messageType' => $messageType, 'message' => $message])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Enumerations\UserRole;
|
||||
use App\Models\User;
|
||||
use App\Repositories\PageTextRepository;
|
||||
use App\Resources\UserResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GlobalDataProvider {
|
||||
private ?User $user;
|
||||
@@ -20,6 +24,57 @@ class GlobalDataProvider {
|
||||
]);
|
||||
}
|
||||
|
||||
public function getInvoiceTypes() : JsonResponse {
|
||||
$invoiceTypes = [];
|
||||
foreach (InvoiceType::all() as $invoiceType) {
|
||||
if (
|
||||
$invoiceType->slug === InvoiceType::INVOICE_TYPE_TRAVELLING ||
|
||||
$invoiceType->slug === InvoiceType::INVOICE_TYPE_OTHER
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$invoiceTypes[] = [
|
||||
'slug' => $invoiceType->slug,
|
||||
'name' => $invoiceType->name
|
||||
];
|
||||
}
|
||||
|
||||
$invoiceTypes[] = ['slug' => InvoiceType::INVOICE_TYPE_OTHER, 'name' => 'Sonstige Kosten'];
|
||||
|
||||
return response()->json([
|
||||
'invoiceTypes' => $invoiceTypes
|
||||
]);
|
||||
}
|
||||
|
||||
public function getMessages() : JsonResponse {
|
||||
$messageContainer = [
|
||||
'message' => '',
|
||||
'type' => ''
|
||||
];
|
||||
|
||||
$message = session()->get('message');
|
||||
|
||||
if (null !== $message) {
|
||||
$message = session()->get('message');
|
||||
session()->forget('message');
|
||||
|
||||
if('' !== $message ) {
|
||||
$messageContainer = unserialize($message);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($messageContainer);
|
||||
}
|
||||
|
||||
public function getTextResourceText(string $textResource) : JsonResponse {
|
||||
$pageTextRepository = new PageTextRepository();
|
||||
|
||||
return response()->json([
|
||||
'content' => $pageTextRepository->getPageText( $textResource)
|
||||
]);
|
||||
}
|
||||
|
||||
private function generateNavbar() : array {
|
||||
$navigation = [
|
||||
'personal' => [],
|
||||
@@ -42,7 +97,7 @@ class GlobalDataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
$navigation['common'][] = ['url' => '/capture-invoice', 'display' => 'Neue Abrechnung'];
|
||||
$navigation['common'][] = ['url' => '/invoice/new', 'display' => 'Neue Abrechnung'];
|
||||
$navigation['common'][] = ['url' => '/available-events', 'display' => 'Verfügbare Veranstaltungen'];
|
||||
|
||||
return $navigation;
|
||||
|
||||
@@ -22,11 +22,6 @@ final class InertiaProvider
|
||||
}
|
||||
|
||||
public function render() : Response {
|
||||
if (null !== session()->get('message')) {
|
||||
$this->props['message'] = session()->get('message');
|
||||
session()->forget('message');
|
||||
}
|
||||
|
||||
$this->props['availableLocalGroups'] = Tenant::where(['is_active_local_group' => true])->get();
|
||||
|
||||
return Inertia::render(
|
||||
|
||||
52
app/Providers/UploadFileProvider.php
Normal file
52
app/Providers/UploadFileProvider.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\CostUnit;
|
||||
use App\ValueObjects\InvoiceFile;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UploadFileProvider {
|
||||
private UploadedFile $file;
|
||||
private CostUnit $costUnit;
|
||||
|
||||
public function __construct(UploadedFile $file, CostUnit $costUnit) {
|
||||
$this->file = $file;
|
||||
$this->costUnit = $costUnit;
|
||||
}
|
||||
|
||||
public function saveUploadedFile() : ?InvoiceFile {
|
||||
try {
|
||||
$directory = sprintf(
|
||||
'%1$s/invoices/%2$s',
|
||||
app('tenant')->slug,
|
||||
$this->costUnit->id
|
||||
);
|
||||
|
||||
$filename = $this->normalizeFilename($this->file->getClientOriginalName());
|
||||
|
||||
$path = $this->file->storeAs(
|
||||
$directory,
|
||||
$filename
|
||||
);
|
||||
|
||||
$invoiceFile = new InvoiceFile();
|
||||
$invoiceFile->filename = $filename;
|
||||
$invoiceFile->path = $path;
|
||||
return $invoiceFile;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeFilename(string $filename) : string {
|
||||
return strtolower($filename)
|
||||
|> function (string $filename) : string { return str_replace(' ', '_', $filename); }
|
||||
|> function (string $filename) : string { return str_replace('ä', 'ae', $filename); }
|
||||
|> function (string $filename) : string { return str_replace('ö', 'oe', $filename); }
|
||||
|> function (string $filename) : string { return str_replace('ü', 'ue', $filename); }
|
||||
|> function (string $filename) : string { return str_replace('ß', 'ss', $filename); };
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,17 @@ use App\Models\CostUnit;
|
||||
use App\Resources\CostUnitResource;
|
||||
|
||||
class CostUnitRepository {
|
||||
public function getCostUnitsForNewInvoice(string $type) : array {
|
||||
return $this->getCostUnitsByCriteria([
|
||||
'allow_new' => true,
|
||||
'type' => $type,
|
||||
'archived' => false
|
||||
], true, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getCurrentEvents() : array {
|
||||
return $this->getCostUnitsByCriteria([
|
||||
'allow_new' => true,
|
||||
@@ -38,8 +49,8 @@ class CostUnitRepository {
|
||||
]);
|
||||
}
|
||||
|
||||
public function getById(int $id) : ?CostUnit {
|
||||
$costUnits = self::getCostUnitsByCriteria(['id' => $id], false);
|
||||
public function getById(int $id, bool $disableAccessCheck = false) : ?CostUnit {
|
||||
$costUnits = self::getCostUnitsByCriteria(['id' => $id], false, $disableAccessCheck);
|
||||
if (count($costUnits) === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -47,7 +58,7 @@ class CostUnitRepository {
|
||||
|
||||
}
|
||||
|
||||
public function getCostUnitsByCriteria(array $criteria, bool $forDisplay = true) : array {
|
||||
public function getCostUnitsByCriteria(array $criteria, bool $forDisplay = true, $disableAccessCheck = false) : array {
|
||||
$tenant = app('tenant');
|
||||
|
||||
$canSeeAll = false;
|
||||
@@ -71,7 +82,7 @@ class CostUnitRepository {
|
||||
$visibleCostUnits = [];
|
||||
/** @var CostUnit $costUnit */
|
||||
foreach (Costunit::where($criteria)->get() as $costUnit) {
|
||||
if ($costUnit->tresurers()->where('user_id', $user->id)->exists() || $canSeeAll) {
|
||||
if ($costUnit->tresurers()->where('user_id', $user->id)->exists() || $canSeeAll || $disableAccessCheck) {
|
||||
if ($forDisplay) {
|
||||
$visibleCostUnits[] = new CostUnitResource($costUnit)->toArray(request());
|
||||
} else {
|
||||
|
||||
19
app/Repositories/PageTextRepository.php
Normal file
19
app/Repositories/PageTextRepository.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\PageText;
|
||||
|
||||
class PageTextRepository {
|
||||
public function getPageText(string $name) : string {
|
||||
$name = strtoupper($name);
|
||||
$pageText = PageText::where(['name' => $name])->first();
|
||||
if (null === $pageText) {
|
||||
PageText::create(['name' => $name, 'content' => '']);
|
||||
|
||||
|
||||
return strtoupper($name);
|
||||
}
|
||||
return $pageText->content;
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,34 @@ class UserRepository {
|
||||
|
||||
return $token === $user->activation_token;
|
||||
}
|
||||
|
||||
public function getCurrentUserDetails() : array {
|
||||
$user = auth()->user();
|
||||
|
||||
$return = [
|
||||
'userId' => null,
|
||||
'userName' => '',
|
||||
'userEmail' => '',
|
||||
'userTelephone' => '',
|
||||
'userAccountOwner' => '',
|
||||
'userAccountIban' => '',
|
||||
];
|
||||
|
||||
if (null !== auth()->user()) {
|
||||
$return = [
|
||||
'userId' => $user->id,
|
||||
'userName' => trim($user->getOfficialName()),
|
||||
'userEmail' => trim($user->email),
|
||||
'userTelephone' => trim($user->phone),
|
||||
'userAccountOwner' => trim($user->bank_account_owner),
|
||||
'userAccountIban' => trim($user->bank_account_iban),
|
||||
];
|
||||
|
||||
if ($return['userAccountOwner'] === '') {
|
||||
$return['userAccountOwner'] = $return['userName'];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,19 @@ namespace App\Scopes;
|
||||
|
||||
use App\Providers\AuthCheckProvider;
|
||||
use App\Repositories\CostUnitRepository;
|
||||
use App\Repositories\PageTextRepository;
|
||||
use App\Repositories\UserRepository;
|
||||
|
||||
abstract class CommonController {
|
||||
protected UserRepository $users;
|
||||
protected CostUnitRepository $costUnits;
|
||||
|
||||
protected PageTextRepository $pageTexts;
|
||||
|
||||
public function __construct() {
|
||||
$this->users = new UserRepository();
|
||||
$this->costUnits = new CostUnitRepository();
|
||||
$this->pageTexts = new PageTextRepository();
|
||||
}
|
||||
|
||||
protected function checkAuth() {
|
||||
|
||||
@@ -22,6 +22,10 @@ class Amount {
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
public function getRoundedAmount() : int {
|
||||
return round($this->amount);
|
||||
}
|
||||
|
||||
public function getCurrency() : string {
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
9
app/ValueObjects/InvoiceFile.php
Normal file
9
app/ValueObjects/InvoiceFile.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\ValueObjects;
|
||||
|
||||
class InvoiceFile {
|
||||
public string $filename;
|
||||
public string $fullPath;
|
||||
|
||||
}
|
||||
56
app/Views/Components/IbanInput.vue
Normal file
56
app/Views/Components/IbanInput.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
const model = defineModel({ type: String, default: '' })
|
||||
|
||||
function onInput(e) {
|
||||
let val = e.target.value
|
||||
|
||||
// alles in Großbuchstaben
|
||||
val = val.toUpperCase()
|
||||
|
||||
// nur Buchstaben, Ziffern und Leerzeichen erlauben
|
||||
val = val.replace(/[^A-Z0-9 ]/g, '')
|
||||
|
||||
// ohne Leerzeichen prüfen
|
||||
const compact = val.replace(/\s+/g, '')
|
||||
|
||||
// max 2 Buchstaben + 20 Ziffern
|
||||
const letters = compact.slice(0, 2).replace(/[^A-Z]/g, '')
|
||||
const digits = compact.slice(2).replace(/[^0-9]/g, '').slice(0, 20)
|
||||
|
||||
// neu zusammensetzen (z. B. alle 4 Zeichen ein Leerzeichen für Lesbarkeit)
|
||||
const formatted = (letters + digits).replace(/(.{4})/g, '$1 ').trim()
|
||||
|
||||
model.value = formatted
|
||||
}
|
||||
|
||||
function onKeypress(e) {
|
||||
const key = e.key
|
||||
|
||||
// immer erlaubt: Leerzeichen
|
||||
if (key === ' ') return
|
||||
|
||||
const compact = model.value.replace(/\s+/g, '')
|
||||
|
||||
if (compact.length < 2) {
|
||||
// in den ersten 2 Stellen nur Buchstaben
|
||||
if (/[A-Za-z]/.test(key)) return
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// danach nur Ziffern bis 20 erlaubt
|
||||
if (/[0-9]/.test(key) && compact.length < 22) return
|
||||
|
||||
e.preventDefault()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
maxlength="27"
|
||||
type="text"
|
||||
:value="model"
|
||||
@input="onInput"
|
||||
@keypress="onKeypress"
|
||||
/>
|
||||
</template>
|
||||
202
app/Views/Components/InfoIcon.vue
Normal file
202
app/Views/Components/InfoIcon.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<!-- InfoIcon.vue -->
|
||||
<template>
|
||||
<span
|
||||
class="info-icon-wrapper"
|
||||
role="button"
|
||||
:aria-label="ariaLabel"
|
||||
tabindex="0"
|
||||
@mouseenter="open"
|
||||
@mouseleave="close"
|
||||
@focus="open"
|
||||
@blur="close"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<slot name="icon">
|
||||
<!-- default info SVG -->
|
||||
<svg class="info-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false">
|
||||
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.08"></circle>
|
||||
<path d="M11 17h2v-6h-2v6zm0-8h2V7h-2v2z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</slot>
|
||||
|
||||
<transition name="fade-scale">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="tooltip"
|
||||
:class="positionClass"
|
||||
role="tooltip"
|
||||
:id="tooltipId"
|
||||
>
|
||||
<div class="tooltip-inner" v-html="text"></div>
|
||||
<!-- small arrow -->
|
||||
<div class="tooltip-arrow" aria-hidden="true"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
text: { type: String, required: true }, // Tooltiptext (HTML erlaubt)
|
||||
position: { type: String, default: 'top' }, // top | right | bottom | left
|
||||
delay: { type: Number, default: 80 }, // ms - delay für Öffnen/Schließen (klein)
|
||||
ariaLabel: { type: String, default: 'Info' }, // aria-label für das Icon (z.B. "Mehr Informationen")
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
let openTimer = null
|
||||
let closeTimer = null
|
||||
|
||||
const tooltipId = `info-icon-tooltip-${Math.round(Math.random()*1e6)}`
|
||||
|
||||
const positionClass = computed(() => {
|
||||
const p = props.position
|
||||
return `pos-${p}`
|
||||
})
|
||||
|
||||
function open() {
|
||||
clearTimeout(closeTimer)
|
||||
openTimer = setTimeout(() => (visible.value = true), props.delay)
|
||||
}
|
||||
|
||||
function close() {
|
||||
clearTimeout(openTimer)
|
||||
closeTimer = setTimeout(() => (visible.value = false), props.delay)
|
||||
}
|
||||
|
||||
function onKeydown(e) {
|
||||
if (e.key === 'Escape' || e.key === 'Esc') {
|
||||
visible.value = false
|
||||
e.stopPropagation()
|
||||
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||
// toggle on Enter / Space
|
||||
e.preventDefault()
|
||||
visible.value = !visible.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-icon-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
cursor: help;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* focus ring */
|
||||
.info-icon-wrapper:focus {
|
||||
box-shadow: 0 0 0 4px rgba(50,115,220,0.12);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* SVG sizing */
|
||||
.info-icon {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Tooltip baseline */
|
||||
/* Tooltip baseline */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
min-width: 180px; /* optional, sorgt für nicht zu kleinen Tooltip */
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
padding: 8px 10px;
|
||||
background: #59a3da;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
|
||||
transform-origin: center;
|
||||
pointer-events: none;
|
||||
white-space: normal; /* erlaubt Umbruch */
|
||||
word-break: break-word; /* lange Wörter umbrechen */
|
||||
}
|
||||
|
||||
/* Arrow */
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
transform: rotate(45deg);
|
||||
background: inherit;
|
||||
box-shadow: inherit;
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
/* Positions */
|
||||
.pos-top {
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.pos-top .tooltip-arrow {
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.pos-bottom {
|
||||
top: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.pos-bottom .tooltip-arrow {
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.pos-left {
|
||||
right: calc(100% + 8px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.pos-left .tooltip-arrow {
|
||||
right: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.pos-right {
|
||||
left: calc(100% + 8px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.pos-right .tooltip-arrow {
|
||||
left: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
/* inner tooltip text styling */
|
||||
.tooltip-inner {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* enter/leave animation */
|
||||
.fade-scale-enter-active,
|
||||
.fade-scale-leave-active {
|
||||
transition: opacity 160ms ease, transform 160ms ease;
|
||||
}
|
||||
.fade-scale-enter-from,
|
||||
.fade-scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.fade-scale-enter-to,
|
||||
.fade-scale-leave-from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
</style>
|
||||
17
app/Views/Components/NumericInput.vue
Normal file
17
app/Views/Components/NumericInput.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- NumericInput.vue -->
|
||||
<script setup>
|
||||
const model = defineModel() // bindet v-model automatisch
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="text"
|
||||
:value="model"
|
||||
@input="model = $event.target.value.replace(/[^0-9]/g, '')"
|
||||
@keypress="($event) => {
|
||||
if (!/[0-9]/.test($event.key)) {
|
||||
$event.preventDefault()
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
29
app/Views/Components/TextResource.vue
Normal file
29
app/Views/Components/TextResource.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
textName: { type: String},
|
||||
belongsTo: { type: String},
|
||||
})
|
||||
|
||||
const contentData = reactive({
|
||||
content: '',
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/core/retrieve-text-resource/' + props.textName);
|
||||
const data = await response.json();
|
||||
Object.assign(contentData, data);
|
||||
console.log(contentData)
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label :for="props.belongsTo">{{contentData.content}}</label>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user