diff --git a/app/Domains/CostUnit/Controllers/CreateController.php b/app/Domains/CostUnit/Controllers/CreateController.php index 8229fd4..d4c6dbe 100644 --- a/app/Domains/CostUnit/Controllers/CreateController.php +++ b/app/Domains/CostUnit/Controllers/CreateController.php @@ -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([]); } diff --git a/app/Domains/CostUnit/Controllers/DistanceAllowanceController.php b/app/Domains/CostUnit/Controllers/DistanceAllowanceController.php new file mode 100644 index 0000000..f081966 --- /dev/null +++ b/app/Domains/CostUnit/Controllers/DistanceAllowanceController.php @@ -0,0 +1,20 @@ +costUnits->getById($costUnitId, true); + if (null !== $costUnit) { + $distanceAllowance = $costUnit->distance_allowance; + } + + return response()->json([ + 'distanceAllowance' => $distanceAllowance + ]); + } +} diff --git a/app/Domains/CostUnit/Routes/api.php b/app/Domains/CostUnit/Routes/api.php index bd8b4b5..38e9186 100644 --- a/app/Domains/CostUnit/Routes/api.php +++ b/app/Domains/CostUnit/Routes/api.php @@ -1,21 +1,21 @@ 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']); }); - - }); }); }); diff --git a/app/Domains/CostUnit/Routes/web.php b/app/Domains/CostUnit/Routes/web.php index ed6271c..715d4c3 100644 --- a/app/Domains/CostUnit/Routes/web.php +++ b/app/Domains/CostUnit/Routes/web.php @@ -1,17 +1,13 @@ group(function () { Route::prefix('cost-unit')->group(function () { diff --git a/app/Domains/Dashboard/Views/Dashboard.vue b/app/Domains/Dashboard/Views/Dashboard.vue index 90371d0..ee947b6 100644 --- a/app/Domains/Dashboard/Views/Dashboard.vue +++ b/app/Domains/Dashboard/Views/Dashboard.vue @@ -1,7 +1,8 @@ diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php new file mode 100644 index 0000000..4d527e8 --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceCommand.php @@ -0,0 +1,63 @@ +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); + } +} diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php new file mode 100644 index 0000000..27cb211 --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceRequest.php @@ -0,0 +1,64 @@ +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; + } + +} diff --git a/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceResponse.php b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceResponse.php new file mode 100644 index 0000000..357fed9 --- /dev/null +++ b/app/Domains/Invoice/Actions/CreateInvoice/CreateInvoiceResponse.php @@ -0,0 +1,15 @@ +success = false; + $this->invoice = null; + } +} diff --git a/app/Domains/Invoice/Controllers/NewInvoiceController.php b/app/Domains/Invoice/Controllers/NewInvoiceController.php new file mode 100644 index 0000000..d8613a1 --- /dev/null +++ b/app/Domains/Invoice/Controllers/NewInvoiceController.php @@ -0,0 +1,138 @@ +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()); + } +} diff --git a/app/Domains/Invoice/Routes/api.php b/app/Domains/Invoice/Routes/api.php new file mode 100644 index 0000000..ea6710c --- /dev/null +++ b/app/Domains/Invoice/Routes/api.php @@ -0,0 +1,25 @@ +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']); + }); + + + + }); +}); diff --git a/app/Domains/Invoice/Routes/web.php b/app/Domains/Invoice/Routes/web.php new file mode 100644 index 0000000..4667b70 --- /dev/null +++ b/app/Domains/Invoice/Routes/web.php @@ -0,0 +1,22 @@ +group(function () { + Route::prefix('invoice')->group(function () { + Route::get('/new', NewInvoiceController::class); + + + + Route::middleware(['auth'])->group(function () { + Route::get('/create', [CreateController::class, 'showForm']); + }); + + + + }); +}); diff --git a/app/Domains/Invoice/Views/CreateInvoice.vue b/app/Domains/Invoice/Views/CreateInvoice.vue deleted file mode 100644 index 01f0f5f..0000000 --- a/app/Domains/Invoice/Views/CreateInvoice.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - Dashboard Content - Test 1! - Hier wird mal eine Rechnung erstellt. - Wenn es geht oder auch nicht - {{props.tenant}} - - Test Toaster - Error Toaster - - - - - diff --git a/app/Domains/Invoice/Views/NewInvoice.vue b/app/Domains/Invoice/Views/NewInvoice.vue new file mode 100644 index 0000000..b75767d --- /dev/null +++ b/app/Domains/Invoice/Views/NewInvoice.vue @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + Bitte auswählen + + {{ event.name }} + + + + {{ event.name }} + + + + + + diff --git a/app/Domains/Invoice/Views/Partials/newInvoice/expense-accounting.vue b/app/Domains/Invoice/Views/Partials/newInvoice/expense-accounting.vue new file mode 100644 index 0000000..d58c907 --- /dev/null +++ b/app/Domains/Invoice/Views/Partials/newInvoice/expense-accounting.vue @@ -0,0 +1,124 @@ + + + + + Wofür hast du den Betrag ausgegeben + + + + {{ availableInvoiceType.name }} + + + + + + + + + + + + Wie hoch ist der Betrag + Euro + + + + + + + + + + + + + + + + + + diff --git a/app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue b/app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue new file mode 100644 index 0000000..5f42d40 --- /dev/null +++ b/app/Domains/Invoice/Views/Partials/newInvoice/refund-data.vue @@ -0,0 +1,157 @@ + + + + + + Dein Name Name (kein Pfadiname): + + + + + E-Mail-Adresse (Für Rückfragen): + + + + + Telefonnummer (für Rückfragen): + + + + + + Möchtest du den Betrag spenden? + + + + + + + + + + + + + + + + Konto-Inhaber*in: + + + + IBAN: + + + + + + + + + + + + + + diff --git a/app/Domains/Invoice/Views/Partials/newInvoice/travel-expense-accounting.vue b/app/Domains/Invoice/Views/Partials/newInvoice/travel-expense-accounting.vue new file mode 100644 index 0000000..440d515 --- /dev/null +++ b/app/Domains/Invoice/Views/Partials/newInvoice/travel-expense-accounting.vue @@ -0,0 +1,158 @@ + + + + + Gib deine Reisesetrecke an + + + + + Bist du mit dem ÖPNV gefahren oder besitzt du einen Beleg + + + + + + + + Wie hoch ist der Betrag? + Euro + + + + + + + + + + Reiseinformationen + Gesamtlänge des Reisewegs: + km + ({{ amount }} km x {{distanceAllowance.toFixed(2).replace('.', ',')}} Euro / km = {{ (amount * distanceAllowance).toFixed(2).replace('.', ',') }} Euro) + + + Ich habe Personen mitgenommen + + + Ich habe Material transportiert + + + + + + + + + + + + + + + diff --git a/app/Domains/UserManagement/Routes/web.php b/app/Domains/UserManagement/Routes/web.php index b7ce619..70ed29e 100644 --- a/app/Domains/UserManagement/Routes/web.php +++ b/app/Domains/UserManagement/Routes/web.php @@ -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); - }); - }); diff --git a/app/Enumerations/InvoiceStatus.php b/app/Enumerations/InvoiceStatus.php new file mode 100644 index 0000000..b8ab0b7 --- /dev/null +++ b/app/Enumerations/InvoiceStatus.php @@ -0,0 +1,20 @@ +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]); + } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php new file mode 100644 index 0000000..4356ebf --- /dev/null +++ b/app/Models/Invoice.php @@ -0,0 +1,68 @@ +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 { diff --git a/app/Providers/AuthCheckProvider.php b/app/Providers/AuthCheckProvider.php index 4cb38a7..c695f77 100644 --- a/app/Providers/AuthCheckProvider.php +++ b/app/Providers/AuthCheckProvider.php @@ -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 { diff --git a/app/Providers/FlashMessageProvider.php b/app/Providers/FlashMessageProvider.php new file mode 100644 index 0000000..487885c --- /dev/null +++ b/app/Providers/FlashMessageProvider.php @@ -0,0 +1,11 @@ +put('message', + serialize(['messageType' => $messageType, 'message' => $message]) + ); + } +} diff --git a/app/Providers/GlobalDataProvider.php b/app/Providers/GlobalDataProvider.php index 41047a0..d3033e4 100644 --- a/app/Providers/GlobalDataProvider.php +++ b/app/Providers/GlobalDataProvider.php @@ -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; diff --git a/app/Providers/InertiaProvider.php b/app/Providers/InertiaProvider.php index 0ae5ebf..69734ad 100644 --- a/app/Providers/InertiaProvider.php +++ b/app/Providers/InertiaProvider.php @@ -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( diff --git a/app/Providers/UploadFileProvider.php b/app/Providers/UploadFileProvider.php new file mode 100644 index 0000000..e0b950f --- /dev/null +++ b/app/Providers/UploadFileProvider.php @@ -0,0 +1,52 @@ +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); }; + + } +} diff --git a/app/Repositories/CostUnitRepository.php b/app/Repositories/CostUnitRepository.php index d3b5fb3..90b6de8 100644 --- a/app/Repositories/CostUnitRepository.php +++ b/app/Repositories/CostUnitRepository.php @@ -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 { diff --git a/app/Repositories/PageTextRepository.php b/app/Repositories/PageTextRepository.php new file mode 100644 index 0000000..dd03841 --- /dev/null +++ b/app/Repositories/PageTextRepository.php @@ -0,0 +1,19 @@ + $name])->first(); + if (null === $pageText) { + PageText::create(['name' => $name, 'content' => '']); + + + return strtoupper($name); + } + return $pageText->content; + } +} diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index a96bfb6..48788ae 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -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; + } } diff --git a/app/Scopes/CommonController.php b/app/Scopes/CommonController.php index 92efb66..cf04fc5 100644 --- a/app/Scopes/CommonController.php +++ b/app/Scopes/CommonController.php @@ -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() { diff --git a/app/ValueObjects/Amount.php b/app/ValueObjects/Amount.php index 11d6256..411f91d 100644 --- a/app/ValueObjects/Amount.php +++ b/app/ValueObjects/Amount.php @@ -22,6 +22,10 @@ class Amount { return $this->amount; } + public function getRoundedAmount() : int { + return round($this->amount); + } + public function getCurrency() : string { return $this->currency; } diff --git a/app/ValueObjects/InvoiceFile.php b/app/ValueObjects/InvoiceFile.php new file mode 100644 index 0000000..55b2a3a --- /dev/null +++ b/app/ValueObjects/InvoiceFile.php @@ -0,0 +1,9 @@ + +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() +} + + + + + diff --git a/app/Views/Components/InfoIcon.vue b/app/Views/Components/InfoIcon.vue new file mode 100644 index 0000000..aca4e99 --- /dev/null +++ b/app/Views/Components/InfoIcon.vue @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/Views/Components/NumericInput.vue b/app/Views/Components/NumericInput.vue new file mode 100644 index 0000000..fdcb10e --- /dev/null +++ b/app/Views/Components/NumericInput.vue @@ -0,0 +1,17 @@ + + + + + { + if (!/[0-9]/.test($event.key)) { + $event.preventDefault() + } + }" + /> + diff --git a/app/Views/Components/TextResource.vue b/app/Views/Components/TextResource.vue new file mode 100644 index 0000000..cd49ad5 --- /dev/null +++ b/app/Views/Components/TextResource.vue @@ -0,0 +1,29 @@ + + + + {{contentData.content}} + + + diff --git a/database/migrations/2026_01_30_140002_create_users_table.php b/database/migrations/2026_01_30_140002_create_users_table.php index 0a9c1ce..3e14a46 100644 --- a/database/migrations/2026_01_30_140002_create_users_table.php +++ b/database/migrations/2026_01_30_140002_create_users_table.php @@ -60,6 +60,7 @@ return new class extends Migration $table->string('eating_habits')->nullable(); $table->string('swimming_permission')->nullable(); $table->string('first_aid_permission')->nullable(); + $table->string('bank_account_owner')->nullable(); $table->string('bank_account_iban')->nullable(); $table->string('activation_token')->nullable(); $table->dateTime('activation_token_expires_at')->nullable()->default(date('Y-m-d H:i:s', strtotime('+3 days'))); diff --git a/database/migrations/2026_01_30_140010_create_invoices.php b/database/migrations/2026_01_30_140010_create_invoices.php new file mode 100644 index 0000000..bc95432 --- /dev/null +++ b/database/migrations/2026_01_30_140010_create_invoices.php @@ -0,0 +1,68 @@ +string('slug')->primary(); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('invoice_status', function (Blueprint $table) { + $table->string('slug')->primary(); + $table->timestamps(); + }); + + Schema::create('invoices', function (Blueprint $table) { + $table->id(); + $table->string('tenant'); + $table->foreignId('cost_unit_id')->constrained('cost_units', 'id')->cascadeOnDelete()->cascadeOnUpdate(); + $table->string('invoice_number'); + $table->string('status'); + $table->string('type'); + $table->string('type_other')->nullable(); + $table->boolean('donation')->default(false); + $table->foreignId('user_id')->nullable()->constrained('users', 'id')->cascadeOnDelete()->cascadeOnUpdate(); + $table->string('contact_name'); + $table->string('contact_email')->nullable(); + $table->string('contact_phone')->nullable(); + $table->string('contact_bank_owner')->nullable(); + $table->string('contact_bank_iban')->nullable(); + $table->float('amount', 2); + $table->integer('distance')->nullable(); + $table->string('comment')->nullable(); + $table->string('changes')->nullable(); + $table->string('travel_direction')->nullable(); + $table->boolean('passengers')->nullable(); + $table->boolean('transportation')->nullable(); + $table->string('document_filename')->nullable(); + $table->foreignId('approved_by')->nullable()->constrained('users', 'id')->cascadeOnDelete()->cascadeOnUpdate(); + $table->dateTime('approved_at')->nullable(); + $table->boolean('upload_required')->default(false); + $table->foreignId('denied_by')->nullable()->constrained('users', 'id')->cascadeOnDelete()->cascadeOnUpdate(); + $table->dateTime('denied_at')->nullable(); + $table->string('denied_reason')->nullable(); + + $table->foreign('tenant')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreign('type')->references('slug')->on('invoice_types')->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreign('status')->references('slug')->on('invoice_status')->cascadeOnDelete()->cascadeOnUpdate(); + + $table->timestamps(); + }); + + } + + public function down(): void + { + Schema::dropIfExists('invoices'); + Schema::dropIfExists('invoice_status'); + Schema::dropIfExists('invoice_types'); + } +}; diff --git a/database/migrations/2026_01_30_140010_create_page_texts.php b/database/migrations/2026_01_30_140010_create_page_texts.php new file mode 100644 index 0000000..39fc8b5 --- /dev/null +++ b/database/migrations/2026_01_30_140010_create_page_texts.php @@ -0,0 +1,24 @@ +id(); + $table->string('name'); + $table->string('content'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('page_texts'); + } +}; diff --git a/public/css/invoices.css b/public/css/invoices.css new file mode 100644 index 0000000..dcf1bfa --- /dev/null +++ b/public/css/invoices.css @@ -0,0 +1,41 @@ +.invoice-main-flexbox { + display: flex; + gap: 20px; + padding: 20px; +} + +.invoice-main-flexbox div { + flex: 1; + padding: 10px; + border: 1px solid #ccc; + border-radius: 10px; + background-color: #ffffff; + min-height: 200px; + box-shadow: 0 0 10px #ccc; + cursor: pointer; +} + +.invoice-main-flexbox div:hover { + background-color: #FAE39C; +} + +fieldset { + background-color: #ffffff; + border-color: #ccc; + border-width: 1px; + border-radius: 10px; + margin: 10px 20px; + padding: 15px 50px 15px 30px; + box-shadow: 0 0 10px #ccc; +} + +fieldset legend { + border-left-style: solid; + border-left-width: 20px; + border-color: #ccc; + border-bottom-style: solid; + border-bottom-width: 1px; + padding: 10px 25px; + background-color: #ffffff; + border-radius: 10px; +} diff --git a/resources/js/components/InvoiceUploadChecks.js b/resources/js/components/InvoiceUploadChecks.js new file mode 100644 index 0000000..bf5c8e8 --- /dev/null +++ b/resources/js/components/InvoiceUploadChecks.js @@ -0,0 +1,23 @@ +export function checkFilesize(fieldId) { + const maxFileSize = 64; + + + if (document.getElementById(fieldId).files[0].size <= maxFileSize * 1024 * 1024) { + return true; + } else { + alert('Die hochzuladende Datei darf die Größe von 64 MB nicht überschreiten'); + return false; + } +} + +export function invoiceCheckContactName() { + const contact_name_val = document.getElementById('contact_name').value.trim() !== ''; + const payment = document.getElementById('confirm_payment'); + + if (contact_name_val && document.getElementById('account_owner').value === '') { + document.getElementById('account_owner').value = document.getElementById('contact_name').value.trim(); + + } else { + payment.style.display = 'none'; + } +} diff --git a/resources/js/components/ajaxHandler.js b/resources/js/components/ajaxHandler.js index a459869..8132a49 100644 --- a/resources/js/components/ajaxHandler.js +++ b/resources/js/components/ajaxHandler.js @@ -8,6 +8,8 @@ export function useAjax() { async function request(url, options = {}) { loading.value = true + const isFormData = options.body instanceof FormData + error.value = null data.value = null @@ -15,14 +17,19 @@ export function useAjax() { const response = await fetch(url, { method: options.method || "GET", headers: { - "Content-Type": "application/json", 'X-CSRF-TOKEN': csrfToken, + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), ...(options.headers || {}), }, - body: options.body ? JSON.stringify(options.body) : null, + body: isFormData + ? options.body // ✅ FormData direkt + : options.body + ? JSON.stringify(options.body) + : null, }) if (!response.ok) throw new Error(`HTTP ${response.status}`) + const result = await response.json() data.value = result return result @@ -35,6 +42,7 @@ export function useAjax() { } } + async function download(url, options = {}) { loading.value = true error.value = null diff --git a/resources/js/layouts/AppLayout.vue b/resources/js/layouts/AppLayout.vue index 644116f..c28c1f1 100644 --- a/resources/js/layouts/AppLayout.vue +++ b/resources/js/layouts/AppLayout.vue @@ -18,14 +18,26 @@ const globalProps = reactive({ user: null, currentPath: '/', errors: {}, - availableLocalGroups: [] + availableLocalGroups: [], + message: '' }); - onMounted(async () => { - const response = await fetch('/api/v1/retreive-global-data'); + const response = await fetch('/api/v1/core/retrieve-global-data'); const data = await response.json(); Object.assign(globalProps, data); + + const messageResponse = await request('/api/v1/core/retrieve-messages', { + method: 'GET', + }) + + if (messageResponse.message !== '') { + if (messageResponse.messageType === 'success') { + toast.success(messageResponse.message) + } else { + toast.error(messageResponse.message) + } + } }); diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 795dbc7..7058306 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -6,6 +6,7 @@ + diff --git a/routes/web.php b/routes/web.php index 95052ec..389c5a7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,16 +3,32 @@ use App\Domains\Dashboard\Controllers\DashboardController; use App\Http\Controllers\TestRenderInertiaProvider; use App\Middleware\IdentifyTenant; +use App\Providers\GlobalDataProvider; use Illuminate\Support\Facades\Route; require_once __DIR__ . '/../app/Domains/UserManagement/Routes/web.php'; require_once __DIR__ . '/../app/Domains/CostUnit/Routes/web.php'; require_once __DIR__ . '/../app/Domains/CostUnit/Routes/api.php'; +require_once __DIR__ . '/../app/Domains/Invoice/Routes/web.php'; +require_once __DIR__ . '/../app/Domains/Invoice/Routes/api.php'; + + + Route::middleware(IdentifyTenant::class)->group(function () { Route::get('/', DashboardController::class); + Route::prefix('api/v1/core') ->group(function () { + Route::get('/retrieve-global-data', GlobalDataProvider::class); + Route::get('/retrieve-text-resource/{textResource}', [GlobalDataProvider::class, 'getTextResourceText']); + Route::get('/retrieve-messages', [GlobalDataProvider::class, 'getMessages']); + Route::get('/retrieve-invoice-types', [GlobalDataProvider::class, 'getInvoiceTypes']); + + }); + + + Route::middleware(['auth'])->group(function () {
Test 1! - Hier wird mal eine Rechnung erstellt. - Wenn es geht oder auch nicht
+ + {{ availableInvoiceType.name }} + +