T2: Database Schema + All Migrations - 10 migrations: users extension, services, songs, song_groups, song_slides, song_arrangements, song_arrangement_groups, service_songs, slides, cts_sync_log - 9 Eloquent models with relationships and casts - 9 factory classes for testing - Tests: DatabaseSchemaTest (2 tests, 26 assertions) ✅ T3: ChurchTools OAuth Provider - Custom Socialite provider for ChurchTools OAuth2 - AuthController with redirect/callback/logout - Replaced Breeze login with OAuth-only (German UI) - Removed all Breeze register/password-reset pages - Tests: OAuthTest (9 tests, 54 assertions) ✅ T4: CTS API Service + Sync Command - ChurchToolsService wrapping 5pm-HDH/churchtools-api - SyncChurchToolsCommand (php artisan cts:sync) - SyncController for refresh button - CCLI-based song matching - Tests: ChurchToolsSyncTest (2 tests) ✅ T5: File Conversion Service - FileConversionService with letterbox/pillarbox to 1920×1080 - ConvertPowerPointJob (queued) with LibreOffice + spatie/pdf-to-image - ZIP extraction and recursive processing - Thumbnail generation (320×180) - Tests: FileConversionTest (2 tests, 21 assertions) ✅ T6: Shared Vue Components - AuthenticatedLayout with nav, user info, refresh button - useAutoSave composable (500ms debounce) - FlashMessage, ConfirmDialog, LoadingSpinner components - HandleInertiaRequests middleware with shared props - Tests: SharedPropsTest (7 tests) ✅ T7: Email Configuration - MissingSongRequest mailable (German) - Email template with song info and service link - SONG_REQUEST_EMAIL config - Tests: MissingSongMailTest (2 tests, 10 assertions) ✅ All tests passing: 30/30 (233 assertions) All UI text in German with 'Du' form Wave 1 complete: 7/7 tasks ✅
101 lines
3.2 KiB
Vue
101 lines
3.2 KiB
Vue
<script setup>
|
|
import { ref, watch, onMounted } from 'vue'
|
|
import { usePage } from '@inertiajs/vue3'
|
|
|
|
const page = usePage()
|
|
const visible = ref(false)
|
|
const message = ref('')
|
|
const type = ref('success')
|
|
let hideTimeout = null
|
|
|
|
function show(msg, msgType) {
|
|
message.value = msg
|
|
type.value = msgType
|
|
visible.value = true
|
|
|
|
if (hideTimeout) clearTimeout(hideTimeout)
|
|
hideTimeout = setTimeout(() => {
|
|
visible.value = false
|
|
}, 4000)
|
|
}
|
|
|
|
function dismiss() {
|
|
visible.value = false
|
|
if (hideTimeout) clearTimeout(hideTimeout)
|
|
}
|
|
|
|
function checkFlash() {
|
|
const flash = page.props.flash
|
|
if (flash?.success) {
|
|
show(flash.success, 'success')
|
|
} else if (flash?.error) {
|
|
show(flash.error, 'error')
|
|
}
|
|
}
|
|
|
|
onMounted(checkFlash)
|
|
|
|
watch(() => page.props.flash, checkFlash, { deep: true })
|
|
</script>
|
|
|
|
<template>
|
|
<Transition
|
|
enter-active-class="transition duration-300 ease-out"
|
|
enter-from-class="translate-y-[-100%] opacity-0"
|
|
enter-to-class="translate-y-0 opacity-100"
|
|
leave-active-class="transition duration-200 ease-in"
|
|
leave-from-class="translate-y-0 opacity-100"
|
|
leave-to-class="translate-y-[-100%] opacity-0"
|
|
>
|
|
<div
|
|
v-if="visible"
|
|
class="fixed left-1/2 top-4 z-[100] w-auto max-w-lg -translate-x-1/2"
|
|
role="alert"
|
|
>
|
|
<div
|
|
class="flex items-center gap-3 rounded-xl px-5 py-3 shadow-lg backdrop-blur-sm"
|
|
:class="{
|
|
'bg-emerald-600/90 text-white': type === 'success',
|
|
'bg-red-600/90 text-white': type === 'error',
|
|
}"
|
|
>
|
|
<!-- Erfolg-Icon -->
|
|
<svg
|
|
v-if="type === 'success'"
|
|
class="h-5 w-5 shrink-0"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
|
|
<!-- Fehler-Icon -->
|
|
<svg
|
|
v-if="type === 'error'"
|
|
class="h-5 w-5 shrink-0"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
|
</svg>
|
|
|
|
<span class="text-sm font-medium">{{ message }}</span>
|
|
|
|
<button
|
|
@click="dismiss"
|
|
class="ml-2 shrink-0 rounded-lg p-1 opacity-70 transition hover:opacity-100"
|
|
aria-label="Schließen"
|
|
>
|
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|