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 ✅
55 lines
1.5 KiB
JavaScript
55 lines
1.5 KiB
JavaScript
import { useDebounceFn } from '@vueuse/core'
|
|
import { router } from '@inertiajs/vue3'
|
|
import { ref } from 'vue'
|
|
|
|
/**
|
|
* Auto-Save Composable
|
|
*
|
|
* Text-Eingaben: Debounce 500ms vor dem Speichern
|
|
* Selects/Checkboxen: Sofortige Speicherung (kein Debounce)
|
|
*
|
|
* @param {string} url - Die URL zum Speichern
|
|
* @param {string} method - HTTP-Methode ('put' oder 'post')
|
|
* @returns {{ save: Function, saveImmediate: Function, saving: Ref<boolean>, saved: Ref<boolean> }}
|
|
*/
|
|
export function useAutoSave(url, method = 'put') {
|
|
const saving = ref(false)
|
|
const saved = ref(false)
|
|
let savedTimeout = null
|
|
|
|
const performSave = (data) => {
|
|
saving.value = true
|
|
saved.value = false
|
|
|
|
router[method](url, data, {
|
|
preserveScroll: true,
|
|
preserveState: true,
|
|
onSuccess: () => {
|
|
saving.value = false
|
|
saved.value = true
|
|
|
|
if (savedTimeout) clearTimeout(savedTimeout)
|
|
savedTimeout = setTimeout(() => {
|
|
saved.value = false
|
|
}, 2000)
|
|
},
|
|
onError: () => {
|
|
saving.value = false
|
|
},
|
|
})
|
|
}
|
|
|
|
// Text-Eingaben: 500ms Debounce
|
|
const save = useDebounceFn((data) => {
|
|
performSave(data)
|
|
}, 500)
|
|
|
|
// Selects/Checkboxen: Sofort speichern
|
|
const saveImmediate = (data) => {
|
|
save.cancel()
|
|
performSave(data)
|
|
}
|
|
|
|
return { save, saveImmediate, saving, saved }
|
|
}
|