Basic design created

This commit is contained in:
2026-02-03 09:33:18 +01:00
parent 3570f442f5
commit e280fcfba8
29 changed files with 1055 additions and 28 deletions

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Domains\Dashboard\Controllers;
use App\Providers\AuthCheckProvider;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class DashboardController extends CommonController {
public function __invoke(Request $request) {
if ($this->checkAuth()) {
return $this->renderForLoggedInUser($request);
}
return redirect()->intended('/login');
dd('U');
return $this->renderForGuest($request);
}
private function renderForLoggedInUser(Request $request) {
$authCheckProvider = new AuthCheckProvider;
$inertiaProvider = new InertiaProvider('Dashboard/Dashboard', ['appName' => app('tenant')->name]);
return $inertiaProvider->render();
}
private function renderForGuest(Request $request) {
}
}

View File

@@ -0,0 +1,42 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
const props = defineProps({
navbar: Object,
tenant: String,
user: Object,
currentPath: String,
})
</script>
<template>
<AppLayout title='Dashboard' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
<diV class="dashboard-widget-container">
<shadowed-box class="dashboard-widget-box" style="width: 60%;">
Meine Anmeldungen
</shadowed-box>
<shadowed-box class="dashboard-widget-box">
Meine Abrechnungen
</shadowed-box>
</diV>
</AppLayout>
</template>
<style>
.dashboard-widget-container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
position: relative;
}
.dashboard-widget-box {
flex-grow: 1; display: inline-block;
height: 150px;
margin: 0 10px;
}
</style>

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Domains\UserManagement\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LogOutController {
public function __invoke(Request $request) {
Auth::logout();
// Session invalidieren
$request->session()->invalidate();
// CSRF-Token regenerieren (für Sicherheit)
$request->session()->regenerateToken();
// Redirect z.B. zur Login-Seite
return redirect()->intended('/')->with('status', 'Erfolgreich abgemeldet!');
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Domains\UserManagement\Controllers;
use App\Providers\InertiaProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController {
public function loginForm(Request $request) {
$errors = [];
if ($request->session()->has('errors')) {
$errors = $request->session()->get('errors')->getBag('default')->getMessages();
}
$inertiaProvider = new InertiaProvider('UserManagement/Login', ['errors' => $errors, 'appName' => app('tenant')->name]);
return $inertiaProvider->render();
}
public function doLogin(Request $request)
{
$credentials = $request->validate([
'username' => ['required', 'string'],
'password' => ['required'],
],
[
'username.required' => 'Bitte gib deinen Anmeldenamen ein.',
'username.string' => 'Der Anmeldename muss eine E-Mail-Adresse sein.',
'password.required' => 'Bitte gib dein Passwort ein.',
]);
#$credentials = ['username' => 'development', 'password' => 'development'];
if (!Auth::attempt($credentials)) {
return back()->withErrors([
'username' => 'Diese Zugangsdaten sind ungültig.',
]);
}
$request->session()->regenerate();
$user = Auth::user();
# dd($user->firstname . ' ' . $user->lastname);
return redirect()->intended('/');
}
}

View File

@@ -0,0 +1,64 @@
<script setup>
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import {onMounted, ref} from 'vue'
import { toast } from 'vue3-toastify'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
const props = defineProps({
navbar: Object,
tenant: String,
user: Object,
currentPath: String,
errors: Object,
})
onMounted(() => {
if (undefined !== props.errors && undefined !== props.errors.username) {
toast.error(props.errors.username[0])
}
})
//console.log(props.errors.password[0])
const username = ref('')
const password = ref('')
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
</script>
<template>
<AppLayout title='Anmelden' :user="props.user" :navbar="props.navbar" :tenant="props.tenant" :currentPath="props.currentPath">
<form method="POST" action="/login">
<input type="hidden" name="_token" :value="csrfToken" />
<shadowed-box style="width: 50%; margin: 150px auto; padding: 20px;">
<table>
<tr>
<th>Anmeldename</th>
<td>
<input type="text" name="username" id="username"></input>
</td>
</tr>
<tr>
<th>Passwort</th>
<td><input type="password" name="password" id="password"></input></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Anmelden" style="margin-top: 20px;" />
</td>
</tr>
</table>
</shadowed-box>
</form>
</AppLayout>
</template>
<style>
th {
width: 100px;
}
</style>

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use App\Providers\InertiaProvider;
class TestRenderInertiaProvider
{
public function index() {
$inertiaProvider = new InertiaProvider('Invoice/CreateInvoice', ['appName' => app('tenant')->name]);
return $inertiaProvider->render();
}
}

View File

@@ -38,6 +38,7 @@ class User extends Authenticatable
'first_aid_permission', 'first_aid_permission',
'bank_account_iban', 'bank_account_iban',
'password', 'password',
'active',
]; ];
/** /**

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Providers;
class AuthCheckProvider {
public function checkLoggedIn() : bool {
if (!auth()->check()) {
return false;
}
$user = auth()->user();
$tenant = app('tenant');
return $user->active && $tenant->slug === $user->tenant;
}
public function getUserRole() : ?string {
if (!$this->checkLoggedIn()) {
return null;
}
return auth()->user()->user_role;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Providers; namespace App\Providers;
use App\Models\User;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
@@ -10,17 +11,43 @@ final class InertiaProvider
private string $vueFile; private string $vueFile;
private array $props; private array $props;
private ?User $user;
public function __construct(string $vueFile, array $props) { public function __construct(string $vueFile, array $props) {
$this->user = auth()->user();
$this->vueFile = $vueFile; $this->vueFile = $vueFile;
$this->props = $props; $this->props = $props;
} }
public function render() : Response { public function render() : Response {
$this->props['navbar'] = $this->generateNavbar();
$this->props['tenant'] = app('tenant')->local_group_name;
$this->props['user'] = $this->user;
$this->props['currentPath'] = request()->path();
return Inertia::render( return Inertia::render(
str_replace('/', '/Views/', $this->vueFile), str_replace('/', '/Views/', $this->vueFile),
$this->props $this->props
); );
} }
private function generateNavbar() : array {
$navigation = [
'personal' => [],
'common' => [],
'costunits' => [],
'events' => [],
];
$navigation['personal'][] = ['url' => '/', 'display' => 'Home'];
if (null !== $this->user) {
$navigation['personal'][] = ['url' => '/personal-data', 'display' => 'Meine Daten'];
$navigation['personal'][] = ['url' => '/messages', 'display' => 'Meine Nachrichten'];
}
$navigation['common'][] = ['url' => '/capture-invoice', 'display' => 'Neue Abrechnung'];
$navigation['common'][] = ['url' => '/available-events', 'display' => 'Verfügbare Veranstaltungen'];
return $navigation;
}
} }

View File

@@ -16,7 +16,11 @@ class TenantUserProvider extends EloquentUserProvider
} }
} }
$query->where('tenant', app('tenant')->slug); $query->where([
'tenant' => app('tenant')->slug,
'active' => true
]);
return $query->first(); return $query->first();
} }

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Scopes;
use App\Providers\AuthCheckProvider;
abstract class CommonController {
protected function checkAuth() {
$authCheckProvider = new AuthCheckProvider;
return $authCheckProvider->checkLoggedIn();
}
}

View File

@@ -0,0 +1,17 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { library } from '@fortawesome/fontawesome-svg-core'
import * as SolidIcons from '@fortawesome/free-solid-svg-icons'
const props = defineProps({
name: { type: String, required: true },
})
if (SolidIcons[`fa${props.name.charAt(0).toUpperCase()}${props.name.slice(1)}`]) {
library.add(SolidIcons[`fa${props.name.charAt(0).toUpperCase()}${props.name.slice(1)}`])
}
</script>
<template>
<font-awesome-icon :icon="name" />
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
const props = defineProps({
style: { type: String},
class: { type: String},
})
console.log(props.style)
</script>
<template>
<div class="shadowed-box" :style=props.style :class=props.class>
<slot></slot>
</div>
</template>
<style>
.shadowed-box {
box-shadow: 2px 2px 5px #c0c0c0;
border-radius: 10px;
padding: 10px;
background-color: #ffffff;
}
</style>

View File

@@ -0,0 +1,41 @@
<script setup>
import ShadowedBox from "../../Components/ShadowedBox.vue";
</script>
<template>
<diV class="widget-container">
<shadowed-box class="widget-box">
Widget 1
</shadowed-box>
<shadowed-box class="widget-box">
Widget 2
</shadowed-box>
<shadowed-box class="widget-box">
Widget 3
</shadowed-box>
<shadowed-box class="widget-box">
Widget 4
</shadowed-box>
</diV>
</template>
<style>
.widget-container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
position: relative;
}
.widget-box {
flex-grow: 1; display: inline-block;
width: calc(25% - 40px);
height: 150px;
margin: 0 10px;
}
</style>

View File

@@ -32,7 +32,7 @@ return [
| |
*/ */
'lifetime' => (int) env('SESSION_LIFETIME', 120), 'lifetime' => (int) env('SESSION_LIFETIME', 43200), // 30 days
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),

View File

@@ -62,7 +62,7 @@ return new class extends Migration
$table->string('first_aid_permission')->nullable(); $table->string('first_aid_permission')->nullable();
$table->string('bank_account_iban')->nullable(); $table->string('bank_account_iban')->nullable();
$table->string('activation_token')->nullable(); $table->string('activation_token')->nullable();
$table->boolean('activated')->default(false); $table->boolean('active')->default(false);
$table->foreign('tenant')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate(); $table->foreign('tenant')->references('slug')->on('tenants')->cascadeOnDelete()->cascadeOnUpdate();
$table->foreign('user_role')->references('slug')->on('user_roles')->cascadeOnDelete()->cascadeOnUpdate(); $table->foreign('user_role')->references('slug')->on('user_roles')->cascadeOnDelete()->cascadeOnUpdate();

View File

@@ -48,6 +48,11 @@ services:
sh -c " sh -c "
npm install && npm install &&
npm install vue3-toastify && npm install @inertiajs/progress && npm install @inertiajs/progress && npm install vue3-toastify && npm install @inertiajs/progress && npm install @inertiajs/progress &&
npm install @fortawesome/fontawesome-svg-core &&
npm install @fortawesome/free-solid-svg-icons &&
npm install @fortawesome/vue-fontawesome@latest &&
while true; do while true; do
npm run build npm run build
echo 'Vite Dev-Server beendet. Neustart in 3 Sekunden...' echo 'Vite Dev-Server beendet. Neustart in 3 Sekunden...'

View File

@@ -30,5 +30,16 @@ server {
fastcgi_index index.php; fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
} }
# In der Nginx-Konfiguration von mareike.local
location /build/assets/ {
# Erlaubt explizit deine Subdomain
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, X-Inertia' always;
# Falls du alle Subdomains von mareike.local erlauben willst:
#
}
} }

218
package-lock.json generated
View File

@@ -5,6 +5,9 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/vue-fontawesome": "^3.1.3",
"@inertiajs/progress": "^0.2.7", "@inertiajs/progress": "^0.2.7",
"@inertiajs/vue3": "^2.3.12", "@inertiajs/vue3": "^2.3.12",
"vue": "^3.5.27", "vue": "^3.5.27",
@@ -13,10 +16,12 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"autoprefixer": "^10.4.24",
"axios": "^1.11.0", "axios": "^1.11.0",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0", "laravel-vite-plugin": "^2.0.0",
"tailwindcss": "^4.0.0", "postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^7.0.7" "vite": "^7.0.7"
} }
}, },
@@ -508,6 +513,49 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
"integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-common-types": "7.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz",
"integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "7.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/vue-fontawesome": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.1.3.tgz",
"integrity": "sha512-OHHUTLPEzdwP8kcYIzhioUdUOjZ4zzmi+midwa4bqscza4OJCOvTKJEHkXNz8PgZ23kWci1HkKVX0bm8f9t9gQ==",
"license": "MIT",
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
"vue": ">= 3.0.0 < 4"
}
},
"node_modules/@inertiajs/core": { "node_modules/@inertiajs/core": {
"version": "2.3.12", "version": "2.3.12",
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.3.12.tgz", "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.3.12.tgz",
@@ -1421,6 +1469,43 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/autoprefixer": {
"version": "10.4.24",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
@@ -1432,6 +1517,50 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/call-bind-apply-helpers": { "node_modules/call-bind-apply-helpers": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -1461,6 +1590,27 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/caniuse-lite": {
"version": "1.0.30001766",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -1612,6 +1762,13 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/electron-to-chromium": {
"version": "1.5.283",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz",
"integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==",
"dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -1802,6 +1959,20 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2315,6 +2486,13 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nprogress": { "node_modules/nprogress": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
@@ -2380,6 +2558,13 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -2659,6 +2844,37 @@
"dev": true, "dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",

View File

@@ -10,13 +10,18 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"autoprefixer": "^10.4.24",
"axios": "^1.11.0", "axios": "^1.11.0",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0", "laravel-vite-plugin": "^2.0.0",
"tailwindcss": "^4.0.0", "postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^7.0.7" "vite": "^7.0.7"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/vue-fontawesome": "^3.1.3",
"@inertiajs/progress": "^0.2.7", "@inertiajs/progress": "^0.2.7",
"@inertiajs/vue3": "^2.3.12", "@inertiajs/vue3": "^2.3.12",
"vue": "^3.5.27", "vue": "^3.5.27",

125
public/css/app.css Normal file
View File

@@ -0,0 +1,125 @@
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
html {
background-color: #FAFAFB !important;
}
.content {
padding: 50px 0px;
}
.main {
margin: 0 auto;
box-shadow: 20px 54px 15px rgba(0, 0, 0, 0.1);
border-radius: 0 10px 0 0;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
height: 80px; /* Höhe anpassen */
align-items: center;
justify-content: space-between;
width: calc(100% - 40px);
}
.header .anonymous-actions {
position: relative;
right: calc(-100% + 330px);
width: 350px;
overflow: hidden;
border-radius: 50px 0 0 50px;
background-color: #FAFAFB !important;
text-align: right;
padding: 10px;
top: -75px;
}
.header .user-info {
position: relative;
right: calc(-100% + 275px);
width: 295px;
overflow: hidden;
border-radius: 50px 0 0 50px;
background-color: #FAFAFB !important;
text-align: right;
padding: 10px;
top: -75px;
}
.header .left-side {
height: 75px;
background: #ffffff;
align-content: center;
padding: 0 50px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
width: 100%;
margin-top: -5px;
}
.flexbox {
display: flex;
background-color: #FAFAFB;
height: 100%;
padding: 0;
gap: 1px;
margin: 0;
}
/* Layout */
.app-layout {
display: flex;
height: 95vh;
margin: 20px;
background: #f0f2f5;
font-family: sans-serif;
}
.sidebar {
flex-basis:275px;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
height: 100%;
background-color: #ffffff !important;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
}
.logo img {
width: 135px !important;
height: 70px !important;
}
.footer {
height: 40px;
background: #666666;
border-top: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #ffffff;
}
th {
text-align: left;
padding-right: 1em;
}
th:after {
content: ":";
}

47
public/css/elements.css Normal file
View File

@@ -0,0 +1,47 @@
/* Toaster */
.toaster {
position: fixed;
bottom: 20px;
right: 20px;
background: #4caf50;
color: #fff;
padding: 12px 20px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
font-size: 13pt;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
color: #656363;
}
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus {
outline: none;
border-color: #1d4899;
}
table {
width: 100%;
}
button, input[type="submit"] {
cursor: pointer;
background-color: #ffffff;
border: 1px solid #809dd5 !important;
padding: 10px 20px;
font-weight: bold;
}
button:hover, input[type="submit"]:hover {
background-color: #1d4899;
color: #ffffff;
}

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -1,11 +0,0 @@
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}

View File

@@ -5,6 +5,15 @@ import { InertiaProgress } from '@inertiajs/progress'
import Vue3Toastify, { toast } from 'vue3-toastify' import Vue3Toastify, { toast } from 'vue3-toastify'
import 'vue3-toastify/dist/index.css' import 'vue3-toastify/dist/index.css'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// Icons importieren
import { faUser, faTrash, faCheck } from '@fortawesome/free-solid-svg-icons'
library.add(faUser, faTrash, faCheck)
InertiaProgress.init() InertiaProgress.init()
createInertiaApp({ createInertiaApp({
@@ -24,12 +33,28 @@ createInertiaApp({
const vueApp = createApp({ render: () => h(App, props) }) const vueApp = createApp({ render: () => h(App, props) })
vueApp.use(plugin) vueApp.use(plugin)
vueApp.component('font-awesome-icon', FontAwesomeIcon)
vueApp.use(Vue3Toastify, { vueApp.use(Vue3Toastify, {
autoClose: 3000, autoClose: 10000,
position: 'top-right', position: 'bottom-right',
pauseOnHover: true, pauseOnHover: true,
hideProgressBar: false, // Progressbar anzeigen
toastDefaults: {
success: {
style: {background: '#4caf50', color: '#fff'}, // grün
progressStyle: {background: '#2e7d32', height: '4px'},
},
error: {
style: {background: '#f44336', color: '#fff'}, // rot
progressStyle: {background: '#c62828', height: '4px'},
},
},
}) })
vueApp.config.globalProperties.$toast = toast vueApp.config.globalProperties.$toast = toast
vueApp.mount(el) vueApp.mount(el)
}, },

View File

@@ -0,0 +1,195 @@
<script setup>
import Icon from "../../../app/Views/Components/Icon.vue";
import GlobalWidgets from "../../../app/Views/Partials/GlobalWidgets/GlobalWidgets.vue";
const props = defineProps({
title: { type: String, default: 'App' },
user: { type: Object, },
flash: { type: Object, default: () => ({}) },
navbar: { type: Object },
tenant: String,
currentPath: String,
})
console.log(props.currentPath)
</script>
<template>
<div class="app-layout">
<!-- Sidebar -->
<!-- Main content -->
<div class="main">
<!-- Header -->
<div class="flexbox">
<div class="sidebar">
<div class="logo">
<img src="../../../public/images/logo.png" alt="Logo" />
</div>
<nav class="nav">
<ul class="nav-links" v-if="props.navbar.personal.length > 0">
<li v-for="navlink in props.navbar.personal">
<a
:class="{ navlink_active: navlink.url.endsWith(props.currentPath) }"
:href="navlink.url"
>{{navlink.display}}</a>
</li>
</ul>
<ul class="nav-links" v-if="props.navbar.common.length > 0">
<li v-for="navlink in props.navbar.common">
<a
:class="{ navlink_active: navlink.url.endsWith(props.currentPath) }"
:href="navlink.url"
>{{navlink.display}}</a>
</li>
</ul>
<ul class="nav-links" v-if="props.navbar.costunits.length > 0">
<li v-for="navlink in props.navbar.costunits">
<a
:class="{ navlink_active: navlink.url.endsWith(props.currentPath) }"
:href="navlink.url"
>{{navlink.display}}</a>
</li>
</ul>
<ul class="nav-links" v-if="props.navbar.events.length > 0">
<li v-for="navlink in props.navbar.events">
<a
:class="{ navlink_active: navlink.url.endsWith(props.currentPath) }"
:href="navlink.url"
>{{navlink.display}}</a>
</li>
</ul>
</nav>
</div>
<div class="main">
<div class="header">
<div class="left-side"><h1>{{ props.title }}</h1></div>
<div class="user-info" v-if="props.user !== null">
<a href="/messages" class="header-link-anonymous" title="Meine Nachrichten">
<Icon name="envelope" />
</a>
<a href="/profile" class="header-link-anonymous" title="Mein Profil">
<Icon name="user" />
</a>
<a href="/logout" class="header-link-anonymous-logout" title="Abmelden">
<Icon name="lock" />
</a>
</div>
<div class="anonymous-actions" v-else>
<a href="/register" class="header-link-anonymous">Registrieren</a>
<a href="/login" class="header-link-anonymous"> Anmelden </a>
</div>
</div>
<global-widgets :user="props.user" :tenant="props.tenant" v-if="props.user !== null" />
<div class="content">
<slot />
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
&copy; 2026 Your Company
</footer>
</div>
<!-- Toaster -->
<transition name="fade">
<div v-if="flash.message" class="toaster">
{{ flash.message }}
</div>
</transition>
</div>
</template>
<style scoped>
.header-link-anonymous,
.header-link-anonymous-logout {
color: #000000;
font-weight: bold;
text-decoration: none;
background-color: #ffffff;
padding: 10px 30px;
margin: 0 -20px 0 30px;
overflow: hidden !important;
}
.header-link-anonymous-logout {
padding-right: 35px !important;
}
.header-link-anonymous:hover {
background-color: #1d4899;
color: #ffffff;
}
.header-link-anonymous-logout:hover {
background-color: #ff0000;
color: #ffffff;
}
.nav-links {
border-bottom: 1px solid #ddd;
width: 285px;
}
.nav-links li a {
color: #b6b6b6;
background-color: #fff;
padding: 20px 25px;
display: block;
text-decoration: none;
width: calc(100% - 50px);
font-weight: bold;
margin-bottom: 0px;
}
.nav {
flex: 1;
margin-left: -10px;
width: 275px;
}
.nav ul {
list-style: none;
padding: 0;
}
.nav li {
margin-bottom: 10px;
}
.nav a {
text-decoration: none;
color: #333;
padding: 8px 12px;
display: block;
transition: background 0.2s;
}
.nav a:hover {
background-color: #1d4899;
color: #ffffff;
}
.navlink_active {
background-color: #fae39c !important;
color: #1d4899 !important; /* Dunklere Schrift beim Hover */
}
</style>

View File

@@ -1,10 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/elements.css" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Base href für relative Assets -->
<base href="/">
<!-- Vite Assets -->
@vite('resources/js/app.js') @vite('resources/js/app.js')
@inertiaHead @inertiaHead
</head> </head>
<body> <body>

View File

@@ -1,16 +1,39 @@
<?php <?php
use App\Domains\Dashboard\Controllers\DashboardController;
use App\Domains\UserManagement\Controllers\LoginController;
use App\Domains\UserManagement\Controllers\LogOutController;
use App\Http\Controllers\TestRenderInertiaProvider;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Inertia\Inertia; use Inertia\Inertia;
Route::get('/', function () { Route::middleware(IdentifyTenant::class)->group(function () {
return view('welcome'); Route::get('/', DashboardController::class);
Route::middleware(['auth'])->group(function () {
Route::get('/messages', fn () => inertia('Messages'));
Route::post('/logout', [LogoutController::class, 'logout']);
}); });
Route::get('/inertia', function () {
return Inertia::render('Pages/Home', [
'appName' => config('app.name'),
]);
route::get('/logout', LogOutController::class);
route::post('/login', [LoginController::class, 'doLogin']);
route::get('/login', [LoginController::class, 'loginForm']);
Route::get('/messages', [TestRenderInertiaProvider::class, 'index']);
}); });

View File

@@ -4,6 +4,7 @@ import laravel from 'laravel-vite-plugin'
import path from 'path' import path from 'path'
export default defineConfig({ export default defineConfig({
base: '',
plugins: [ plugins: [
laravel({ laravel({
input: 'resources/js/app.js', input: 'resources/js/app.js',
@@ -18,4 +19,9 @@ export default defineConfig({
'@domains': path.resolve(__dirname, 'app/Domains'), '@domains': path.resolve(__dirname, 'app/Domains'),
}, },
}, },
server: {
host: true, // Dev-Server auch über Subdomains erreichbar
strictPort: true, // verhindert zufällige Portwechsel
cors: true, // erlaubt Dev-HMR über Subdomains
},
}) })