- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
317 lines
15 KiB
PHP
Executable File
317 lines
15 KiB
PHP
Executable File
<x-layouts.app :title="__('ui.dashboard')">
|
|
<h1 class="text-2xl font-bold mb-6">{{ __('events.hello_user', ['name' => auth()->user()->name]) }}</h1>
|
|
|
|
{{-- Kalender --}}
|
|
<div x-data="calendarApp()" class="mb-8">
|
|
{{-- Header: View-Toggle + Navigation --}}
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
|
|
<h2 class="text-lg font-semibold">{{ __('events.calendar') }}</h2>
|
|
<div class="flex items-center gap-2">
|
|
<button @click="goToToday()" class="px-3 py-1.5 text-xs border border-gray-300 rounded-md hover:bg-gray-50">{{ __('events.today') }}</button>
|
|
<div class="flex bg-gray-100 rounded-md p-0.5">
|
|
<button @click="view = 'month'" :class="view === 'month' ? 'bg-white shadow text-gray-900' : 'text-gray-500 hover:text-gray-700'" class="px-3 py-1.5 text-xs font-medium rounded-md transition">{{ __('events.month_view') }}</button>
|
|
<button @click="view = 'year'" :class="view === 'year' ? 'bg-white shadow text-gray-900' : 'text-gray-500 hover:text-gray-700'" class="px-3 py-1.5 text-xs font-medium rounded-md transition">{{ __('events.year_view') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ===== MONATSANSICHT ===== --}}
|
|
<template x-if="view === 'month'">
|
|
<div class="bg-white rounded-lg shadow">
|
|
{{-- Monats-Navigation --}}
|
|
<div class="flex items-center justify-between px-4 py-3 border-b">
|
|
<button @click="prevMonth()" class="p-1 hover:bg-gray-100 rounded-md">
|
|
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
|
</button>
|
|
<h3 class="text-base font-semibold text-gray-900" x-text="monthName + ' ' + currentYear"></h3>
|
|
<button @click="nextMonth()" class="p-1 hover:bg-gray-100 rounded-md">
|
|
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
{{-- Wochentage --}}
|
|
<div class="grid grid-cols-7 border-b">
|
|
<template x-for="day in weekDayNames" :key="day">
|
|
<div class="px-1 py-2 text-center text-xs font-medium text-gray-500 uppercase" x-text="day"></div>
|
|
</template>
|
|
</div>
|
|
|
|
{{-- Tage --}}
|
|
<div class="grid grid-cols-7">
|
|
<template x-for="(day, index) in calendarDays" :key="index">
|
|
<div
|
|
:class="{
|
|
'bg-gray-50 text-gray-400': !day.currentMonth,
|
|
'bg-blue-50': day.isToday,
|
|
'min-h-[80px] sm:min-h-[100px]': true
|
|
}"
|
|
class="border-b border-r p-1 text-xs"
|
|
>
|
|
<div class="flex items-center justify-between mb-0.5">
|
|
<span
|
|
:class="day.isToday ? 'bg-blue-600 text-white w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold' : 'text-gray-700 px-1'"
|
|
x-text="day.dayNum"
|
|
></span>
|
|
</div>
|
|
<div class="space-y-0.5">
|
|
<template x-for="evt in eventsForDate(day.dateStr)" :key="evt.id">
|
|
<a :href="evt.url" class="flex items-center gap-0.5 truncate rounded px-1 py-0.5 text-[10px] leading-tight font-medium hover:opacity-80" :class="eventBgClass(evt.type)">
|
|
<span x-text="evt.time + ' ' + evt.typeLabel"></span>
|
|
<span class="ml-auto flex gap-px shrink-0 tabular-nums">
|
|
<span class="text-green-700" x-text="evt.tl.y"></span>
|
|
<span class="text-red-700" x-text="evt.tl.n"></span>
|
|
<span class="opacity-50" x-text="evt.tl.o"></span>
|
|
</span>
|
|
</a>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
{{-- ===== JAHRESANSICHT ===== --}}
|
|
<template x-if="view === 'year'">
|
|
<div>
|
|
{{-- Jahres-Navigation --}}
|
|
<div class="flex items-center justify-center gap-4 mb-4">
|
|
<button @click="currentYear--" class="p-1 hover:bg-gray-100 rounded-md">
|
|
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
|
</button>
|
|
<h3 class="text-base font-semibold text-gray-900" x-text="currentYear"></h3>
|
|
<button @click="currentYear++" class="p-1 hover:bg-gray-100 rounded-md">
|
|
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
{{-- 12 Mini-Monate --}}
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
|
<template x-for="m in 12" :key="m">
|
|
<div class="bg-white rounded-lg shadow p-3 cursor-pointer hover:shadow-md transition-shadow" @click="currentMonth = m - 1; view = 'month'">
|
|
<h4 class="text-xs font-semibold text-gray-700 mb-2" x-text="getMonthName(m - 1)"></h4>
|
|
{{-- Mini-Wochentage --}}
|
|
<div class="grid grid-cols-7 gap-px mb-1">
|
|
<template x-for="wd in weekDayLetters" :key="wd">
|
|
<div class="text-center text-[9px] text-gray-400" x-text="wd"></div>
|
|
</template>
|
|
</div>
|
|
{{-- Mini-Tage --}}
|
|
<div class="grid grid-cols-7 gap-px">
|
|
<template x-for="(d, i) in miniMonthDays(m - 1)" :key="i">
|
|
<div class="flex items-center justify-center h-4 w-full">
|
|
<template x-if="d.dayNum">
|
|
<span
|
|
class="w-3.5 h-3.5 flex items-center justify-center rounded-full text-[8px] leading-none"
|
|
:class="d.isToday ? 'bg-blue-600 text-white font-bold' : (d.eventType ? eventDotClass(d.eventType) : 'text-gray-600')"
|
|
x-text="d.dayNum"
|
|
></span>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<script>
|
|
function calendarApp() {
|
|
const today = new Date();
|
|
return {
|
|
view: 'month',
|
|
currentMonth: today.getMonth(),
|
|
currentYear: today.getFullYear(),
|
|
events: @js($calendarEvents),
|
|
locale: document.documentElement.lang || 'de',
|
|
|
|
// Index der Events nach Datum für schnellen Zugriff
|
|
_eventIndex: null,
|
|
get eventIndex() {
|
|
if (!this._eventIndex) {
|
|
this._eventIndex = {};
|
|
this.events.forEach(e => {
|
|
if (!this._eventIndex[e.date]) this._eventIndex[e.date] = [];
|
|
this._eventIndex[e.date].push(e);
|
|
});
|
|
}
|
|
return this._eventIndex;
|
|
},
|
|
|
|
get monthName() {
|
|
return this.getMonthName(this.currentMonth);
|
|
},
|
|
|
|
getMonthName(month) {
|
|
return new Date(2024, month).toLocaleDateString(this.locale, { month: 'long' });
|
|
},
|
|
|
|
get weekDayNames() {
|
|
const days = [];
|
|
// Montag = 1 als erster Tag
|
|
for (let i = 1; i <= 7; i++) {
|
|
const d = new Date(2024, 0, i); // 2024-01-01 ist Montag
|
|
days.push(d.toLocaleDateString(this.locale, { weekday: 'short' }));
|
|
}
|
|
return days;
|
|
},
|
|
|
|
get weekDayLetters() {
|
|
return this.weekDayNames.map(d => d.charAt(0).toUpperCase());
|
|
},
|
|
|
|
get calendarDays() {
|
|
const year = this.currentYear;
|
|
const month = this.currentMonth;
|
|
const firstDay = new Date(year, month, 1);
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
|
|
// Wochentag des 1. (0=So, 1=Mo, ..., 6=Sa) → Umrechnung auf Mo=0
|
|
let startWeekDay = firstDay.getDay() - 1;
|
|
if (startWeekDay < 0) startWeekDay = 6;
|
|
|
|
const days = [];
|
|
|
|
// Tage vom Vormonat
|
|
const prevMonthLast = new Date(year, month, 0);
|
|
for (let i = startWeekDay - 1; i >= 0; i--) {
|
|
const dayNum = prevMonthLast.getDate() - i;
|
|
const d = new Date(year, month - 1, dayNum);
|
|
days.push({
|
|
dayNum: dayNum,
|
|
dateStr: this.formatDate(d),
|
|
currentMonth: false,
|
|
isToday: this.isToday(d)
|
|
});
|
|
}
|
|
|
|
// Tage des aktuellen Monats
|
|
for (let d = 1; d <= lastDay.getDate(); d++) {
|
|
const date = new Date(year, month, d);
|
|
days.push({
|
|
dayNum: d,
|
|
dateStr: this.formatDate(date),
|
|
currentMonth: true,
|
|
isToday: this.isToday(date)
|
|
});
|
|
}
|
|
|
|
// Tage des nächsten Monats (Rest auffüllen bis Vielfaches von 7)
|
|
const remaining = 7 - (days.length % 7);
|
|
if (remaining < 7) {
|
|
for (let d = 1; d <= remaining; d++) {
|
|
const date = new Date(year, month + 1, d);
|
|
days.push({
|
|
dayNum: d,
|
|
dateStr: this.formatDate(date),
|
|
currentMonth: false,
|
|
isToday: this.isToday(date)
|
|
});
|
|
}
|
|
}
|
|
|
|
return days;
|
|
},
|
|
|
|
miniMonthDays(month) {
|
|
const year = this.currentYear;
|
|
const firstDay = new Date(year, month, 1);
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
|
|
let startWeekDay = firstDay.getDay() - 1;
|
|
if (startWeekDay < 0) startWeekDay = 6;
|
|
|
|
const days = [];
|
|
|
|
// Leere Zellen vor dem 1.
|
|
for (let i = 0; i < startWeekDay; i++) {
|
|
days.push({ dayNum: 0, eventType: null, isToday: false });
|
|
}
|
|
|
|
// Tage
|
|
for (let d = 1; d <= lastDay.getDate(); d++) {
|
|
const date = new Date(year, month, d);
|
|
const dateStr = this.formatDate(date);
|
|
const dayEvents = this.eventIndex[dateStr];
|
|
days.push({
|
|
dayNum: d,
|
|
eventType: dayEvents ? dayEvents[0].type : null,
|
|
isToday: this.isToday(date)
|
|
});
|
|
}
|
|
|
|
return days;
|
|
},
|
|
|
|
eventsForDate(dateStr) {
|
|
return this.eventIndex[dateStr] || [];
|
|
},
|
|
|
|
formatDate(date) {
|
|
const y = date.getFullYear();
|
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
const d = String(date.getDate()).padStart(2, '0');
|
|
return y + '-' + m + '-' + d;
|
|
},
|
|
|
|
isToday(date) {
|
|
return date.getFullYear() === today.getFullYear()
|
|
&& date.getMonth() === today.getMonth()
|
|
&& date.getDate() === today.getDate();
|
|
},
|
|
|
|
prevMonth() {
|
|
if (this.currentMonth === 0) {
|
|
this.currentMonth = 11;
|
|
this.currentYear--;
|
|
} else {
|
|
this.currentMonth--;
|
|
}
|
|
},
|
|
|
|
nextMonth() {
|
|
if (this.currentMonth === 11) {
|
|
this.currentMonth = 0;
|
|
this.currentYear++;
|
|
} else {
|
|
this.currentMonth++;
|
|
}
|
|
},
|
|
|
|
goToToday() {
|
|
this.currentMonth = today.getMonth();
|
|
this.currentYear = today.getFullYear();
|
|
},
|
|
|
|
// CSS-Klassen für Events in der Monatsansicht
|
|
eventBgClass(type) {
|
|
const map = {
|
|
home_game: 'bg-blue-100 text-blue-800',
|
|
away_game: 'bg-indigo-100 text-indigo-800',
|
|
training: 'bg-green-100 text-green-800',
|
|
tournament: 'bg-purple-100 text-purple-800',
|
|
meeting: 'bg-yellow-100 text-yellow-800',
|
|
other: 'bg-gray-100 text-gray-800'
|
|
};
|
|
return map[type] || 'bg-gray-100 text-gray-800';
|
|
},
|
|
|
|
// CSS-Klassen für Punkte in der Jahresansicht
|
|
eventDotClass(type) {
|
|
const map = {
|
|
home_game: 'bg-blue-500 text-white',
|
|
away_game: 'bg-indigo-500 text-white',
|
|
training: 'bg-green-500 text-white',
|
|
tournament: 'bg-purple-500 text-white',
|
|
meeting: 'bg-yellow-500 text-white',
|
|
other: 'bg-gray-400 text-white'
|
|
};
|
|
return map[type] || 'bg-gray-400 text-white';
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
</x-layouts.app>
|