6 Commits

12 changed files with 204 additions and 61 deletions
@@ -64,7 +64,7 @@ const props = defineProps({
<th style="padding-bottom: 20px" colspan="2">Förderung</th> <th style="padding-bottom: 20px" colspan="2">Förderung</th>
<td style="padding-bottom: 20px" colspan="2"> <td style="padding-bottom: 20px" colspan="2">
{{ props.event.supportPerson.readable }}<br /> {{ props.event.supportPerson.readable }}<br />
<label style="font-size: 9pt;">({{ props.event.supportPersonIndex }} / Tag p.P.)</label> <label style="font-size: 9pt;">({{ props.event.supportPersonValue }} / Tag p.P.)</label>
</td> </td>
</tr> </tr>
@@ -98,7 +98,7 @@ const props = defineProps({
<tr> <tr>
<th style="padding-top: 20px; font-size: 12pt !important;" colspan="2">Budget</th> <th style="padding-top: 20px; font-size: 12pt !important;" colspan="2">Budget</th>
<td v-if="props.event.totalBalance.expected.value >= 0" style="color: #4caf50; font-weight: bold; padding-top: 20px; font-size: 12pt !important;"> <td v-if="props.event.totalBalance.estimated.value >= 0" style="color: #4caf50; font-weight: bold; padding-top: 20px; font-size: 12pt !important;">
{{ props.event.totalBalance.estimated.readable }} {{ props.event.totalBalance.estimated.readable }}
</td> </td>
<td v-else style="color: #f44336; font-weight: bold; padding-top: 20px; font-size: 12pt !important;"> <td v-else style="color: #f44336; font-weight: bold; padding-top: 20px; font-size: 12pt !important;">
@@ -30,7 +30,7 @@ class CreateInvoiceCommand {
'type' => $this->request->invoiceType, 'type' => $this->request->invoiceType,
'type_other' => $this->request->invoiceTypeExtended, 'type_other' => $this->request->invoiceTypeExtended,
'donation' => $this->request->isDonation, 'donation' => $this->request->isDonation,
'user_id' => $this->request->userId, 'user_id' => $this->request->paymentPurpose === null ? $this->request->userId : null,
'contact_name' => $this->request->contactName, 'contact_name' => $this->request->contactName,
'contact_email' => $this->request->contactEmail, 'contact_email' => $this->request->contactEmail,
'contact_phone' => $this->request->contactPhone, 'contact_phone' => $this->request->contactPhone,
@@ -2,6 +2,7 @@
namespace App\Domains\UserManagement\Controllers; namespace App\Domains\UserManagement\Controllers;
use App\Enumerations\UserRole;
use App\Providers\InertiaProvider; use App\Providers\InertiaProvider;
use App\Scopes\CommonController; use App\Scopes\CommonController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -38,20 +39,44 @@ class LoginController extends CommonController {
return redirect()->intended('/register/verifyEmail'); return redirect()->intended('/register/verifyEmail');
} }
#$credentials = ['username' => 'development', 'password' => 'development'];
if (!Auth::attempt($credentials)) { if (!Auth::attempt($credentials)) {
return back()->withErrors([ return back()->withErrors([
'username' => 'Diese Zugangsdaten sind ungültig.', 'username' => 'Diese Zugangsdaten sind ungültig.',
]); ]);
} }
$request->session()->regenerate();
$user = Auth::user(); $user = Auth::user();
$tenant = app('tenant');
// Auf "lv" darf sich grundsätzlich jeder aktive Nutzer einloggen.
// Auf Sub-Tenants gilt:
// - Der Nutzer muss dem Tenant zugeordnet sein (local_group)
// - ODER er hat "Bundesrecht über Landesrecht":
// user_role_main === ROLE_ADMINISTRATOR -> Login auf jedem Sub-Tenant erlaubt.
$isMainAdmin = $user->user_role_main === UserRole::USER_ROLE_ADMIN;
$isMemberOfTenant = $tenant->slug === $user->local_group;
# dd($user->firstname . ' ' . $user->lastname); if ($tenant->slug !== 'lv' && !$isMainAdmin && !$isMemberOfTenant) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return back()->withErrors([
'username' => 'Diese Zugangsdaten sind für diesen Stamm nicht gültig.',
]);
}
if (!$user->active) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return back()->withErrors([
'username' => 'Dieses Benutzerkonto ist nicht aktiv.',
]);
}
$request->session()->regenerate();
return redirect()->intended('/'); return redirect()->intended('/');
} }
+33 -3
View File
@@ -3,6 +3,7 @@
namespace App\Providers; namespace App\Providers;
use App\Enumerations\UserRole; use App\Enumerations\UserRole;
use App\Models\User;
class AuthCheckProvider { class AuthCheckProvider {
public function checkLoggedIn() : bool { public function checkLoggedIn() : bool {
@@ -16,7 +17,7 @@ class AuthCheckProvider {
return $user->active; return $user->active;
} }
if ($user->user_role_main === UserRole::USER_ROLE_ADMIN) { if ($this->isMainAdministrator($user)) {
return true; return true;
} }
@@ -28,10 +29,39 @@ class AuthCheckProvider {
return null; return null;
} }
$user = auth()->user();
if (app('tenant')->slug === 'lv') { if (app('tenant')->slug === 'lv') {
return auth()->user()->user_role_main; return $user->user_role_main;
} }
return auth()->user()->user_role_local_group; // "Bundesrecht steht über Landesrecht":
// Ein ROLE_ADMINISTRATOR auf LV-Ebene ist auf jedem Sub-Tenant automatisch Administrator,
// unabhängig von user_role_local_group.
if ($this->isMainAdministrator($user)) {
return UserRole::USER_ROLE_ADMIN;
}
return $user->user_role_local_group;
}
/**
* Gibt true zurück, wenn der Nutzer auf LV-Ebene Administrator ist.
* Diese Rolle hebt das lokale Rechtesystem für alle Sub-Tenants auf.
*/
public function isMainAdministrator(?User $user = null) : bool {
$user ??= auth()->user();
return $user !== null
&& $user->user_role_main === UserRole::USER_ROLE_ADMIN;
}
/**
* Bequemer Helper für die Berechtigungs-Checks im gesamten System.
* Gibt true zurück, wenn der aktuell eingeloggte Nutzer im Kontext des
* aktuellen Tenants effektiv Administrator ist.
*/
public function isAdministrator() : bool {
return $this->getUserRole() === UserRole::USER_ROLE_ADMIN;
} }
} }
+43 -25
View File
@@ -26,28 +26,7 @@ class GlobalDataProvider {
'tenant' => app('tenant'), 'tenant' => app('tenant'),
'activeUsers' => $this->getActiveUsers(), 'activeUsers' => $this->getActiveUsers(),
'version' => config('app.version'), 'version' => config('app.version'),
]); 'currentEvent' => $this->getCurrentEventData(),
}
public function getAllInvoiceTypes() : JsonResponse {
$invoiceTypes = [];
foreach (InvoiceType::orderBy('sort_order')->get() as $invoiceType) {
if (
$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
]); ]);
} }
@@ -99,10 +78,47 @@ class GlobalDataProvider {
]); ]);
} }
private function getCurrentEventData() : ?array {
if (null === $this->user) {
return null;
}
$currentEvent = new EventRepository()->getMyCurrentEvent();
if (null === $currentEvent) {
return null;
}
return [
'identifier' => $currentEvent->identifier,
'name' => $currentEvent->name,
];
}
public function getAllInvoiceTypes() : JsonResponse {
$invoiceTypes = [];
foreach (InvoiceType::orderBy('sort_order')->get() as $invoiceType) {
if (
$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
]);
}
private function generateNavbar() : array { private function generateNavbar() : array {
$eventRepository = new EventRepository(); $eventRepository = new EventRepository();
$navigation = [ $navigation = [
'personal' => [], 'personal' => [],
'common' => [], 'common' => [],
@@ -116,9 +132,11 @@ class GlobalDataProvider {
$navigation['personal'][] = ['url' => '/personal-data', 'display' => 'Meine Daten']; $navigation['personal'][] = ['url' => '/personal-data', 'display' => 'Meine Daten'];
$navigation['personal'][] = ['url' => '/messages', 'display' => 'Meine Nachrichten']; $navigation['personal'][] = ['url' => '/messages', 'display' => 'Meine Nachrichten'];
$authCheck = new AuthCheckProvider();
$effectiveRole = $authCheck->getUserRole();
if ( if (
in_array($this->user->user_role_local_group, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER] ) || in_array($effectiveRole, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER], true)
$this->user->user_role_main === UserRole::USER_ROLE_ADMIN
) { ) {
$navigation['costunits'][] = ['url' => '/cost-unit/list', 'display' => 'Kostenstellen']; $navigation['costunits'][] = ['url' => '/cost-unit/list', 'display' => 'Kostenstellen'];
$navigation['costunits'][] = ['url' => '/cost-unit/create', 'display' => 'Neue laufende Tätigkeit']; $navigation['costunits'][] = ['url' => '/cost-unit/create', 'display' => 'Neue laufende Tätigkeit'];
+11 -5
View File
@@ -2,6 +2,7 @@
namespace App\Providers; namespace App\Providers;
use App\Enumerations\UserRole;
use Illuminate\Auth\EloquentUserProvider; use Illuminate\Auth\EloquentUserProvider;
class TenantUserProvider extends EloquentUserProvider class TenantUserProvider extends EloquentUserProvider
@@ -18,15 +19,20 @@ class TenantUserProvider extends EloquentUserProvider
} }
} }
// Auf "lv" gilt grundsätzlich keine local_group-Einschränkung.
if (app('tenant')->slug === 'lv') { if (app('tenant')->slug === 'lv') {
return $query->first(); return $query->first();
} }
$query->where([ // Auf Sub-Tenants:
'local_group' => app('tenant')->slug, // - Entweder gehört der Nutzer zum aktuellen Tenant (local_group)
'active' => true // - ODER er ist auf LV-Ebene Administrator
// -> "Bundesrecht steht über Landesrecht": Login überall möglich.
]); $query->where('active', true)
->where(function ($q) {
$q->where('local_group', app('tenant')->slug)
->orWhere('user_role_main', UserRole::USER_ROLE_ADMIN);
});
return $query->first(); return $query->first();
} }
+3 -2
View File
@@ -7,6 +7,7 @@ use App\Enumerations\InvoiceStatus;
use App\Enumerations\InvoiceType; use App\Enumerations\InvoiceType;
use App\Enumerations\UserRole; use App\Enumerations\UserRole;
use App\Models\CostUnit; use App\Models\CostUnit;
use App\Providers\AuthCheckProvider;
use App\Resources\CostUnitResource; use App\Resources\CostUnitResource;
use App\ValueObjects\Amount; use App\ValueObjects\Amount;
use Illuminate\Database\Capsule\Manager as Capsule; use Illuminate\Database\Capsule\Manager as Capsule;
@@ -75,8 +76,8 @@ class CostUnitRepository {
} else { } else {
if ($tenant->slug !== 'lv') { if ($tenant->slug !== 'lv') {
if ( if (
$user->user_role_main === UserRole::USER_ROLE_ADMIN || new AuthCheckProvider()->isAdministrator() ||
in_array($user->user_role_local_group, [UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_ADMIN]) $user->user_role_local_group === UserRole::USER_ROLE_ADMIN
) { ) {
$canSeeAll = true; $canSeeAll = true;
} }
+18 -2
View File
@@ -6,6 +6,7 @@ use App\Enumerations\ParticipationType;
use App\Enumerations\UserRole; use App\Enumerations\UserRole;
use App\Models\CostUnit; use App\Models\CostUnit;
use App\Models\Event; use App\Models\Event;
use App\Providers\AuthCheckProvider;
use App\Resources\CostUnitResource; use App\Resources\CostUnitResource;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -19,13 +20,25 @@ class EventRepository {
} }
public function getMyCurrentEvent() : ?Event {
$events = $this->getEventsByCriteria([
['archived', '=', false],
['start_date', '<=', now()],
['end_date', '>=', now()],
], true);
if (count($events) !== 1) {
return null;
}
return $events[0];
}
public function getUpcoming(int $maxCount = 5, bool $accessCheck = true) : array { public function getUpcoming(int $maxCount = 5, bool $accessCheck = true) : array {
$events = []; $events = [];
foreach ( $this->getEventsByCriteria([ foreach ( $this->getEventsByCriteria([
'archived' => false, 'archived' => false,
],$accessCheck) as $event) { ],$accessCheck) as $event) {
if ($event->start_date > now()) { if ($event->end_date >= now()) {
$event = $event->toResource()->toArray(new Request()); $event = $event->toResource()->toArray(new Request());
$events[] = $event; $events[] = $event;
@@ -72,7 +85,10 @@ class EventRepository {
if (!$accessCheck) { if (!$accessCheck) {
$canSeeAll = true; $canSeeAll = true;
} else { } else {
if ($tenant->slug !== 'lv') { if (
new AuthCheckProvider()->isAdministrator() ||
$user->user_role_local_group === UserRole::USER_ROLE_ADMIN
) {
if ( if (
$user->user_role_main === UserRole::USER_ROLE_ADMIN || $user->user_role_main === UserRole::USER_ROLE_ADMIN ||
in_array($user->user_role_local_group, [UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_ADMIN]) in_array($user->user_role_local_group, [UserRole::USER_ROLE_GROUP_LEADER, UserRole::USER_ROLE_ADMIN])
+4 -7
View File
@@ -26,17 +26,14 @@ class EventParticipantResource extends JsonResource
} }
$presenceDays = $this->resource->arrival_date->diff($this->resource->departure_date)->days; $presenceDays = $this->resource->arrival_date->diff($this->resource->departure_date)->days;
$presenceDaysSupport = $presenceDays; if ($presenceDays === 0) {
$presenceDays = 1;
if ($presenceDaysSupport === 0) {
$presenceDaysSupport = 1; $presenceDaysSupport = 1;
} else { } else {
$presenceDaysSupport = $presenceDaysSupport - 1; $presenceDaysSupport = $presenceDays;
$presenceDays++;
} }
return array_merge( return array_merge(
$this->resource->toArray(), $this->resource->toArray(),
[ [
+1
View File
@@ -89,6 +89,7 @@ class EventResource extends JsonResource{
$returnArray['totalParticipantCount'] = $this->event->participants()->count(); $returnArray['totalParticipantCount'] = $this->event->participants()->count();
$returnArray['supportPersonIndex'] = $this->event->support_per_person->toString(); $returnArray['supportPersonIndex'] = $this->event->support_per_person->toString();
$returnArray['supportPersonValue'] = $this->event->support_per_person->getAmount();
$returnArray['supportPerson'] = $this->calculateSupportPerPerson($returnArray['participants']); $returnArray['supportPerson'] = $this->calculateSupportPerPerson($returnArray['participants']);
$returnArray['income'] = $this->calculateIncomes($returnArray['participants'], $returnArray['supportPerson']['amount']); $returnArray['income'] = $this->calculateIncomes($returnArray['participants'], $returnArray['supportPerson']['amount']);
+56 -7
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import {reactive, onMounted, ref} from 'vue'; import {reactive, onMounted, ref, computed} from 'vue';
import Icon from "../../../app/Views/Components/Icon.vue"; import Icon from "../../../app/Views/Components/Icon.vue";
import GlobalWidgets from "../../../app/Views/Partials/GlobalWidgets/GlobalWidgets.vue"; import GlobalWidgets from "../../../app/Views/Partials/GlobalWidgets/GlobalWidgets.vue";
import {toast} from "vue3-toastify"; import {toast} from "vue3-toastify";
@@ -20,7 +20,8 @@ const globalProps = reactive({
currentPath: '/', currentPath: '/',
errors: {}, errors: {},
availableLocalGroups: [], availableLocalGroups: [],
message: '' message: '',
currentEvent: null,
}); });
const sidebarOpen = ref(false); const sidebarOpen = ref(false);
@@ -51,13 +52,15 @@ onMounted(async () => {
} }
}); });
const currentPath = window.location.pathname; const currentPath = window.location.pathname;
const showCurrentEventLink = computed(() => {
if (!globalProps.currentEvent) {
return false;
}
return currentPath !== '/event/details/' + globalProps.currentEvent.identifier;
});
const props = defineProps({ const props = defineProps({
title: { type: String, default: 'App' }, title: { type: String, default: 'App' },
flash: { type: Object, default: () => ({}) } flash: { type: Object, default: () => ({}) }
@@ -84,6 +87,16 @@ const props = defineProps({
<label id="show_username" v-if="globalProps.user !== null">Willkommen, {{ globalProps.user.nicename }}</label> <label id="show_username" v-if="globalProps.user !== null">Willkommen, {{ globalProps.user.nicename }}</label>
</div> </div>
<a
v-if="showCurrentEventLink"
:href="'/event/details/' + globalProps.currentEvent.identifier"
class="current-event-link"
:title="'Zur Veranstaltung: ' + globalProps.currentEvent.name"
>
<Icon name="calendar-day" />
<span class="current-event-link-label">{{ globalProps.currentEvent.name }}</span>
</a>
<div class="header-actions" v-if="globalProps.user !== null"> <div class="header-actions" v-if="globalProps.user !== null">
<div class="user-info"> <div class="user-info">
<a href="/messages" class="header-link-anonymous" title="Meine Nachrichten"> <a href="/messages" class="header-link-anonymous" title="Meine Nachrichten">
@@ -394,6 +407,34 @@ const props = defineProps({
align-items: center; align-items: center;
} }
/* ─── Direktlink zum aktuellen Event ─── */
.current-event-link {
display: none; /* per Default ausgeblendet nur auf Mobile sichtbar */
align-items: center;
gap: 6px;
color: #1d4899;
font-weight: bold;
text-decoration: none;
padding: 6px 10px;
border-radius: 4px;
margin-right: 10px;
max-width: 50%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.current-event-link:hover {
background-color: #1d4899;
color: #ffffff;
}
.current-event-link-label {
overflow: hidden;
text-overflow: ellipsis;
}
/* ═══════════════════════════════════════════ /* ═══════════════════════════════════════════
TABLET (640px 1023px) TABLET (640px 1023px)
═══════════════════════════════════════════ */ ═══════════════════════════════════════════ */
@@ -455,6 +496,14 @@ const props = defineProps({
height: 60px; height: 60px;
} }
.current-event-link {
display: inline-flex;
}
.current-event-link-label {
max-width: 120px;
}
.left-side h1 { .left-side h1 {
font-size: 1rem; font-size: 1rem;
} }
+1 -1
View File
@@ -1 +1 @@
4.4.0 4.4.1