From db8a012f73b846f1062fdfc9231b19f0868b2e44 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 18 Apr 2026 20:53:15 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20Aziros=20v1.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 6 + .gitignore | 15 + deploy.sh | 22 + docker-compose.local.yml | 169 + docker-compose.yml | 166 + docker/nginx/default.conf | 54 + docker/nginx/local.conf | 58 + docker/ntfy/cache/server.yml | 17 + docker/php/Dockerfile | 60 + src/.editorconfig | 18 + src/.env.example | 130 + src/.gitattributes | 11 + src/.gitignore | 25 + src/CLAUDE.md | 84 + src/README.md | 58 + .../Commands/CheckExpiredSubscriptions.php | 49 + src/app/Console/Commands/CleanMailQueue.php | 48 + .../Console/Commands/CleanupDeletedUsers.php | 33 + .../Commands/ConsolidateSubscriptions.php | 81 + .../Commands/CreateMissingAffiliates.php | 30 + .../ProcessAffiliateQualifications.php | 80 + src/app/Console/Commands/ProcessMailQueue.php | 196 + .../Commands/ProcessMonthlyCredits.php | 59 + src/app/Console/Commands/RunAutomations.php | 103 + .../Commands/ScheduleEventReminders.php | 246 + src/app/Enums/PaymentStatus.php | 51 + src/app/Enums/SubscriptionStatus.php | 49 + src/app/Enums/UserRole.php | 79 + src/app/Enums/UserStatus.php | 10 + src/app/Events/CalendarUpdated.php | 30 + src/app/Events/NotificationCreated.php | 41 + src/app/Helpers/helpers.php | 42 + .../Controllers/Api/AgentChatController.php | 320 + .../Http/Controllers/Api/AuthController.php | 87 + .../Controllers/Api/AutomationController.php | 125 + .../Controllers/Api/ContactController.php | 81 + .../Controllers/Api/DashboardController.php | 63 + .../Http/Controllers/Api/DeviceController.php | 86 + .../Http/Controllers/Api/EventController.php | 116 + .../Http/Controllers/Api/NoteController.php | 79 + .../Controllers/Api/SettingsController.php | 207 + .../Http/Controllers/Api/TaskController.php | 139 + .../Controllers/Api/TranslationController.php | 21 + src/app/Http/Controllers/Controller.php | 8 + .../Integration/GoogleCalendarController.php | 90 + .../Integration/OutlookCalendarController.php | 96 + .../Controllers/Stripe/WebhookController.php | 29 + src/app/Http/Controllers/VerifyController.php | 78 + .../GoogleCalendarWebhookController.php | 53 + src/app/Http/Middleware/CheckAppVersion.php | 22 + .../Http/Middleware/EnsureAuthenticated.php | 46 + src/app/Http/Middleware/EnsureIsAdmin.php | 19 + .../Http/Middleware/EnsureUserIsVerified.php | 25 + src/app/Http/Middleware/ForceJsonResponse.php | 17 + .../RedirectIfAuthenticatedCustom.php | 24 + src/app/Http/Middleware/RoleMiddleware.php | 38 + src/app/Http/Middleware/SetLocale.php | 44 + .../Http/Middleware/TrackAffiliateCode.php | 18 + src/app/Http/Middleware/TrustProxies.php | 29 + src/app/Jobs/SendEventReminder.php | 138 + src/app/Jobs/SyncFromGoogleCalendarJob.php | 52 + src/app/Listeners/GiveBonusCredits.php | 68 + src/app/Livewire/Activities/Index.php | 73 + src/app/Livewire/Admin/Affiliates/Index.php | 69 + src/app/Livewire/Admin/Dashboard.php | 24 + .../Livewire/Admin/Features/Modals/Form.php | 95 + src/app/Livewire/Admin/Plans/Index.php | 35 + src/app/Livewire/Admin/Plans/Modals/Form.php | 191 + src/app/Livewire/Admin/Translations/Index.php | 141 + .../Admin/Translations/Modals/DeleteModal.php | 42 + .../Admin/Translations/Modals/EditModal.php | 54 + src/app/Livewire/Admin/Users/Calendar.php | 46 + src/app/Livewire/Admin/Users/Detail.php | 314 + src/app/Livewire/Admin/Users/Index.php | 75 + src/app/Livewire/Admin/Versions.php | 83 + src/app/Livewire/Agent/History.php | 36 + src/app/Livewire/Agent/Index.php | 433 + src/app/Livewire/Agent/Logs.php | 65 + .../Livewire/Agent/Modals/ConflictModal.php | 139 + src/app/Livewire/Auth/ForgotPassword.php | 50 + src/app/Livewire/Auth/Login.php | 55 + src/app/Livewire/Auth/Modals/VerifyModal.php | 40 + src/app/Livewire/Auth/Register.php | 94 + src/app/Livewire/Auth/ResetPassword.php | 61 + src/app/Livewire/Auth/Verify.php | 125 + src/app/Livewire/Auth/VerifyNotice.php | 422 + src/app/Livewire/Automation/Index.php | 168 + src/app/Livewire/Calendar/Forms/EventForm.php | 293 + src/app/Livewire/Calendar/Index.php | 894 ++ .../Calendar/Modals/DayEventsModal.php | 29 + .../Livewire/Calendar/Modals/EventModal.php | 823 ++ src/app/Livewire/Calendar/Sidebar.php | 64 + src/app/Livewire/Calendar/WeekCanvas.php | 662 ++ src/app/Livewire/Checkout/Index.php | 136 + src/app/Livewire/Checkout/Success.php | 65 + src/app/Livewire/Contacts/Index.php | 154 + src/app/Livewire/Dashboard/Index.php | 141 + src/app/Livewire/Homepage/Agb.php | 14 + src/app/Livewire/Homepage/Datenschutz.php | 14 + src/app/Livewire/Homepage/Example.php | 25 + src/app/Livewire/Homepage/Impressum.php | 14 + src/app/Livewire/Homepage/Index.php | 21 + src/app/Livewire/Homepage/Kontakt.php | 36 + src/app/Livewire/Homepage/Preise.php | 30 + src/app/Livewire/Homepage/UeberUns.php | 14 + src/app/Livewire/Integration/Index.php | 127 + src/app/Livewire/Integration/Modals/Form.php | 13 + src/app/Livewire/Invoices/Index.php | 43 + src/app/Livewire/Notes/Index.php | 126 + src/app/Livewire/Notifications/Bell.php | 99 + src/app/Livewire/Payments/Index.php | 13 + src/app/Livewire/Plans/Index.php | 42 + src/app/Livewire/Settings/Index.php | 386 + .../Settings/Modals/DeleteAccount.php | 42 + .../Livewire/Settings/Modals/SmtpError.php | 19 + src/app/Livewire/Subscription/Index.php | 43 + src/app/Livewire/Tasks/Index.php | 210 + src/app/Mail/AffiliateQualifiedMail.php | 37 + src/app/Mail/AriaComposedMail.php | 34 + src/app/Mail/GiftAccessMail.php | 40 + src/app/Mail/ResetPasswordMail.php | 39 + src/app/Mail/SmtpTestMail.php | 29 + src/app/Models/Activity.php | 187 + src/app/Models/Affiliate.php | 71 + src/app/Models/AffiliateCreditLog.php | 36 + src/app/Models/AffiliateReferral.php | 51 + src/app/Models/AgentLog.php | 66 + src/app/Models/ApiToken.php | 31 + src/app/Models/AppVersion.php | 43 + src/app/Models/Automation.php | 59 + src/app/Models/CalendarIntegration.php | 74 + src/app/Models/Contact.php | 94 + src/app/Models/CreditTransaction.php | 96 + src/app/Models/Device.php | 38 + src/app/Models/EmailLog.php | 19 + src/app/Models/Event.php | 121 + src/app/Models/EventDurationStat.php | 30 + src/app/Models/EventKeyword.php | 29 + src/app/Models/Feature.php | 53 + src/app/Models/FeatureGroup.php | 35 + src/app/Models/MailQueue.php | 49 + src/app/Models/Note.php | 69 + src/app/Models/Notification.php | 28 + src/app/Models/Payment.php | 100 + src/app/Models/Plan.php | 101 + src/app/Models/Reminder.php | 43 + src/app/Models/Subscription.php | 90 + src/app/Models/Task.php | 119 + src/app/Models/Translation.php | 48 + src/app/Models/User.php | 340 + src/app/Models/Verification.php | 54 + .../EventReminderNotification.php | 35 + .../TaskReminderNotification.php | 31 + src/app/Observers/EventObserver.php | 72 + src/app/Providers/AppServiceProvider.php | 51 + src/app/Services/AgentAIService.php | 678 ++ src/app/Services/AgentActionService.php | 732 ++ src/app/Services/AgentContextService.php | 153 + src/app/Services/AgentParserService.php | 114 + src/app/Services/DurationLearningService.php | 226 + src/app/Services/EventPlannerService.php | 863 ++ src/app/Services/GoogleCalendarService.php | 459 + src/app/Services/MailService.php | 42 + src/app/Services/MailerService.php | 131 + src/app/Services/PushService.php | 87 + src/app/Services/StripeService.php | 924 ++ src/app/Services/VerificationService.php | 86 + src/app/View/Components/AppHeader.php | 26 + src/artisan | 18 + src/bootstrap/app.php | 55 + src/bootstrap/providers.php | 7 + src/composer.json | 93 + src/composer.lock | 9568 +++++++++++++++++ src/config/ai_models.php | 30 + src/config/app.php | 138 + src/config/auth.php | 117 + src/config/automations.php | 133 + src/config/broadcasting.php | 82 + src/config/cache.php | 130 + src/config/database.php | 184 + src/config/filesystems.php | 80 + src/config/livewire.php | 282 + src/config/logging.php | 132 + src/config/mail.php | 171 + src/config/queue.php | 129 + src/config/reverb.php | 102 + src/config/services.php | 64 + src/config/session.php | 233 + src/config/sidebar.php | 109 + src/config/wire-elements-modal.php | 52 + src/database/.gitignore | 1 + src/database/factories/UserFactory.php | 45 + .../0001_01_01_000000_create_users_table.php | 73 + .../0001_01_01_000001_create_cache_table.php | 35 + .../0001_01_01_000002_create_jobs_table.php | 57 + ...26_04_03_223546_create_reminders_table.php | 48 + ...6_04_03_223948_create_activities_table.php | 48 + ...026_04_03_224035_create_contacts_table.php | 45 + ...2026_04_03_224117_create_devices_table.php | 47 + .../2026_04_03_231554_create_plans_table.php | 44 + ...4_03_231605_create_subscriptions_table.php | 63 + ...026_04_03_231620_create_payments_table.php | 49 + ...04_04_003244_create_translations_table.php | 33 + ..._04_04_223341_create_mail_queues_table.php | 50 + ...4_05_195243_create_verifications_table.php | 42 + ...6_04_05_220804_create_agent_logs_table.php | 54 + .../2026_04_05_223224_create_events_table.php | 53 + ..._06_212207_create_feature_groups_table.php | 32 + ...026_04_06_212208_create_features_table.php | 40 + ...04_06_212250_create_feature_plan_table.php | 36 + ...4352_create_event_duration_stats_table.php | 36 + ..._07_173346_create_event_keywords_table.php | 34 + ...6_04_08_181424_create_event_user_table.php | 30 + ...001_create_calendar_integrations_table.php | 43 + ..._04_12_000001_create_automations_table.php | 32 + .../2026_04_12_100001_create_notes_table.php | 30 + .../2026_04_12_100002_create_tasks_table.php | 32 + ...6_04_15_000002_create_affiliate_tables.php | 61 + ...00001_create_credit_transactions_table.php | 44 + ...6_04_15_200001_create_api_tokens_table.php | 28 + ...4_17_100003_create_notifications_table.php | 29 + ..._17_100004_create_sent_reminders_table.php | 24 + ...6_04_17_100005_create_email_logs_table.php | 25 + ...4_18_000004_create_event_contact_table.php | 22 + ...sent_reminders_nullable_event_and_type.php | 35 + ..._104656_add_reminder_at_to_tasks_table.php | 26 + ...026_04_18_160645_create_versions_table.php | 28 + src/database/seeders/DatabaseSeeder.php | 25 + .../seeders/ErrorTranslationSeeder.php | 71 + src/database/seeders/FeatureGroupSeeder.php | 70 + src/database/seeders/FeatureSeeder.php | 102 + src/database/seeders/InternalPlanSeeder.php | 37 + .../seeders/MigrateBonusCreditsSeeder.php | 51 + src/database/seeders/PaymentSeeder.php | 98 + src/database/seeders/PlanSeeder.php | 37 + src/database/seeders/RolePlansSeeder.php | 92 + src/database/seeders/TranslationSeeder.php | 1004 ++ src/package-lock.json | 2084 ++++ src/package.json | 25 + src/phpunit.xml | 36 + src/public/.htaccess | 25 + src/public/favicon.ico | 0 src/public/images/logo-text.png | Bin 0 -> 9455 bytes src/public/images/logo-white.png | Bin 0 -> 14175 bytes src/public/images/logo.png | Bin 0 -> 14393 bytes src/public/index.php | 20 + src/public/robots.txt | 2 + src/resources/css/app.css | 123 + src/resources/css/safelist-tailwindcss.txt | 5 + .../fonts/BaiJamjuree/bai-jamjuree-200.woff2 | Bin 0 -> 10428 bytes .../BaiJamjuree/bai-jamjuree-200italic.woff2 | Bin 0 -> 11844 bytes .../fonts/BaiJamjuree/bai-jamjuree-300.woff2 | Bin 0 -> 10728 bytes .../BaiJamjuree/bai-jamjuree-300italic.woff2 | Bin 0 -> 12028 bytes .../fonts/BaiJamjuree/bai-jamjuree-500.woff2 | Bin 0 -> 10808 bytes .../BaiJamjuree/bai-jamjuree-500italic.woff2 | Bin 0 -> 12176 bytes .../fonts/BaiJamjuree/bai-jamjuree-600.woff2 | Bin 0 -> 10840 bytes .../BaiJamjuree/bai-jamjuree-600italic.woff2 | Bin 0 -> 12076 bytes .../fonts/BaiJamjuree/bai-jamjuree-700.woff2 | Bin 0 -> 10656 bytes .../BaiJamjuree/bai-jamjuree-700italic.woff2 | Bin 0 -> 11828 bytes .../BaiJamjuree/bai-jamjuree-italic.woff2 | Bin 0 -> 11908 bytes .../BaiJamjuree/bai-jamjuree-regular.woff2 | Bin 0 -> 10632 bytes src/resources/fonts/BaiJamjuree/font.css | 97 + src/resources/fonts/Space/font.css | 7 + src/resources/fonts/Space/space age.otf | Bin 0 -> 18496 bytes src/resources/fonts/Space/space age.ttf | Bin 0 -> 26748 bytes src/resources/fonts/Syne/Syne-Bold.woff | Bin 0 -> 39528 bytes src/resources/fonts/Syne/Syne-Bold.woff2 | Bin 0 -> 31564 bytes src/resources/fonts/Syne/Syne-ExtraBold.woff | Bin 0 -> 40220 bytes src/resources/fonts/Syne/Syne-ExtraBold.woff2 | Bin 0 -> 32176 bytes src/resources/fonts/Syne/Syne-Italic.woff | Bin 0 -> 70692 bytes src/resources/fonts/Syne/Syne-Italic.woff2 | Bin 0 -> 60680 bytes src/resources/fonts/Syne/Syne-Regular.woff | Bin 0 -> 36604 bytes src/resources/fonts/Syne/Syne-Regular.woff2 | Bin 0 -> 29164 bytes .../fonts/Syne/SyneMono-Regular.woff | Bin 0 -> 33692 bytes .../fonts/Syne/SyneMono-Regular.woff2 | Bin 0 -> 27276 bytes src/resources/fonts/Syne/font.css | 32 + src/resources/js/agent-voice-orb.js | 523 + src/resources/js/app.js | 47 + src/resources/js/bootstrap.js | 4 + src/resources/js/calendar-interact.js | 1432 +++ src/resources/js/calendar-week-canvas.js | 385 + src/resources/js/websocket/connection.js | 17 + src/resources/plugins/Notification/index.js | 2 + .../plugins/Notification/src/config.js | 86 + .../plugins/Notification/src/message.css | 380 + .../plugins/Notification/src/message.js | 251 + .../views/components/app-header.blade.php | 36 + .../views/components/header.blade.php | 125 + src/resources/views/components/logo.blade.php | 67 + .../views/components/sidebar.blade.php | 239 + .../views/components/sidebar/link.blade.php | 30 + .../emails/affiliate-qualified.blade.php | 28 + .../views/emails/agent/message.blade.php | 26 + .../views/emails/agent/reminder.blade.php | 56 + .../views/emails/aria-composed.blade.php | 3 + .../views/emails/auth/verify.blade.php | 29 + .../views/emails/components/button.blade.php | 14 + .../views/emails/components/code.blade.php | 25 + .../views/emails/components/footer.blade.php | 12 + .../views/emails/components/header.blade.php | 11 + .../views/emails/gift-access.blade.php | 35 + src/resources/views/emails/layout.blade.php | 31 + .../views/emails/reset-password.blade.php | 63 + .../views/emails/smtp-test.blade.php | 2 + src/resources/views/errors/400.blade.php | 44 + src/resources/views/errors/401.blade.php | 44 + src/resources/views/errors/403.blade.php | 44 + src/resources/views/errors/404.blade.php | 44 + src/resources/views/errors/500.blade.php | 44 + src/resources/views/errors/501.blade.php | 44 + src/resources/views/errors/502.blade.php | 44 + src/resources/views/layouts/app.blade.php | 102 + src/resources/views/layouts/blank.blade.php | 42 + .../views/livewire/activities/index.blade.php | 241 + .../livewire/admin/affiliates/index.blade.php | 170 + .../views/livewire/admin/dashboard.blade.php | 247 + .../admin/features/modals/form.blade.php | 160 + .../livewire/admin/plans/index.blade.php | 199 + .../admin/plans/modals/form.blade.php | 203 + .../admin/translations/index.blade.php | 131 + .../modals/delete-modal.blade.php | 45 + .../translations/modals/edit-modal.blade.php | 50 + .../livewire/admin/users/calendar.blade.php | 226 + .../livewire/admin/users/detail.blade.php | 522 + .../livewire/admin/users/index.blade.php | 206 + .../views/livewire/admin/versions.blade.php | 133 + .../views/livewire/agent/history.blade.php | 70 + .../views/livewire/agent/index.blade.php | 386 + .../views/livewire/agent/logs.blade.php | 367 + .../agent/modals/conflict-modal.blade.php | 126 + .../livewire/auth/forgot-password.blade.php | 83 + .../views/livewire/auth/login.blade.php | 117 + .../auth/modals/verify-modal.blade.php | 28 + .../views/livewire/auth/register.blade.php | 132 + .../livewire/auth/reset-password.blade.php | 82 + .../livewire/auth/verify-notice.blade.php | 94 + .../views/livewire/auth/verify.blade.php | 184 + .../views/livewire/automation/index.blade.php | 649 ++ .../livewire/calendar/day-canvas.blade.php | 184 + .../calendar/forms/event-form.blade.php | 338 + .../views/livewire/calendar/index.blade.php | 314 + .../modals/day-events-modal.blade.php | 63 + .../calendar/modals/event-modal.blade.php | 362 + .../livewire/calendar/month-canvas.blade.php | 114 + .../views/livewire/calendar/sidebar.blade.php | 93 + .../livewire/calendar/skeletons/day.blade.php | 34 + .../calendar/skeletons/month.blade.php | 43 + .../calendar/skeletons/week.blade.php | 64 + .../calendar/views/day-canvas.blade.php | 184 + .../calendar/views/month-canvas.blade.php | 114 + .../calendar/views/week-canvas.blade.php | 129 + .../livewire/calendar/views_alt/day.blade.php | 101 + .../calendar/views_alt/month.blade.php | 643 ++ .../calendar/views_alt/week.blade.php | 1130 ++ .../livewire/calendar/week-canvas.blade.php | 231 + .../views/livewire/checkout/index.blade.php | 341 + .../views/livewire/checkout/success.blade.php | 155 + .../views/livewire/contacts/index.blade.php | 332 + .../views/livewire/dashboard/index.blade.php | 429 + .../views/livewire/homepage/agb.blade.php | 163 + .../livewire/homepage/datenschutz.blade.php | 201 + .../views/livewire/homepage/example.blade.php | 845 ++ .../livewire/homepage/impressum.blade.php | 137 + .../views/livewire/homepage/index.blade.php | 973 ++ .../views/livewire/homepage/kontakt.blade.php | 217 + .../views/livewire/homepage/preise.blade.php | 352 + .../livewire/homepage/ueber-uns.blade.php | 276 + .../livewire/integration/index.blade.php | 212 + .../integration/modals/form.blade.php | 3 + .../integration/modals/provider.blade.php | 230 + .../views/livewire/invoices/index.blade.php | 236 + .../views/livewire/notes/index.blade.php | 195 + .../livewire/notifications/bell.blade.php | 182 + .../views/livewire/payments/index.blade.php | 3 + .../views/livewire/plans/index.blade.php | 289 + .../views/livewire/settings/index.blade.php | 695 ++ .../settings/modals/delete-account.blade.php | 56 + .../settings/modals/smtp-error.blade.php | 46 + .../livewire/subscription/index.blade.php | 292 + .../views/livewire/tasks/index.blade.php | 346 + .../views/partials/homepage/footer.blade.php | 95 + .../views/partials/homepage/navbar.blade.php | 37 + .../wire-elements-modal/modal.blade.php | 57 + src/resources/views/welcome.blade.php | 225 + src/routes/api.php | 163 + src/routes/channels.php | 15 + src/routes/connect.php | 14 + src/routes/console.php | 45 + src/routes/web.php | 93 + src/routes/www.php | 14 + src/storage/app/.gitignore | 4 + src/storage/app/private/.gitignore | 2 + src/storage/app/public/.gitignore | 2 + src/storage/framework/.gitignore | 9 + src/storage/framework/testing/.gitignore | 2 + src/tests/Feature/AutomationTest.php | 467 + src/tests/Feature/ExampleTest.php | 19 + src/tests/TestCase.php | 10 + src/tests/Unit/ExampleTest.php | 16 + src/vite.config.js | 53 + 400 files changed, 58280 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100755 deploy.sh create mode 100644 docker-compose.local.yml create mode 100644 docker-compose.yml create mode 100644 docker/nginx/default.conf create mode 100644 docker/nginx/local.conf create mode 100644 docker/ntfy/cache/server.yml create mode 100644 docker/php/Dockerfile create mode 100644 src/.editorconfig create mode 100644 src/.env.example create mode 100644 src/.gitattributes create mode 100644 src/.gitignore create mode 100644 src/CLAUDE.md create mode 100644 src/README.md create mode 100644 src/app/Console/Commands/CheckExpiredSubscriptions.php create mode 100644 src/app/Console/Commands/CleanMailQueue.php create mode 100644 src/app/Console/Commands/CleanupDeletedUsers.php create mode 100644 src/app/Console/Commands/ConsolidateSubscriptions.php create mode 100644 src/app/Console/Commands/CreateMissingAffiliates.php create mode 100644 src/app/Console/Commands/ProcessAffiliateQualifications.php create mode 100644 src/app/Console/Commands/ProcessMailQueue.php create mode 100644 src/app/Console/Commands/ProcessMonthlyCredits.php create mode 100644 src/app/Console/Commands/RunAutomations.php create mode 100644 src/app/Console/Commands/ScheduleEventReminders.php create mode 100644 src/app/Enums/PaymentStatus.php create mode 100644 src/app/Enums/SubscriptionStatus.php create mode 100644 src/app/Enums/UserRole.php create mode 100644 src/app/Enums/UserStatus.php create mode 100644 src/app/Events/CalendarUpdated.php create mode 100644 src/app/Events/NotificationCreated.php create mode 100644 src/app/Helpers/helpers.php create mode 100644 src/app/Http/Controllers/Api/AgentChatController.php create mode 100644 src/app/Http/Controllers/Api/AuthController.php create mode 100644 src/app/Http/Controllers/Api/AutomationController.php create mode 100644 src/app/Http/Controllers/Api/ContactController.php create mode 100644 src/app/Http/Controllers/Api/DashboardController.php create mode 100644 src/app/Http/Controllers/Api/DeviceController.php create mode 100644 src/app/Http/Controllers/Api/EventController.php create mode 100644 src/app/Http/Controllers/Api/NoteController.php create mode 100644 src/app/Http/Controllers/Api/SettingsController.php create mode 100644 src/app/Http/Controllers/Api/TaskController.php create mode 100644 src/app/Http/Controllers/Api/TranslationController.php create mode 100644 src/app/Http/Controllers/Controller.php create mode 100644 src/app/Http/Controllers/Integration/GoogleCalendarController.php create mode 100644 src/app/Http/Controllers/Integration/OutlookCalendarController.php create mode 100644 src/app/Http/Controllers/Stripe/WebhookController.php create mode 100644 src/app/Http/Controllers/VerifyController.php create mode 100644 src/app/Http/Controllers/Webhooks/GoogleCalendarWebhookController.php create mode 100644 src/app/Http/Middleware/CheckAppVersion.php create mode 100644 src/app/Http/Middleware/EnsureAuthenticated.php create mode 100644 src/app/Http/Middleware/EnsureIsAdmin.php create mode 100644 src/app/Http/Middleware/EnsureUserIsVerified.php create mode 100644 src/app/Http/Middleware/ForceJsonResponse.php create mode 100644 src/app/Http/Middleware/RedirectIfAuthenticatedCustom.php create mode 100644 src/app/Http/Middleware/RoleMiddleware.php create mode 100644 src/app/Http/Middleware/SetLocale.php create mode 100644 src/app/Http/Middleware/TrackAffiliateCode.php create mode 100644 src/app/Http/Middleware/TrustProxies.php create mode 100644 src/app/Jobs/SendEventReminder.php create mode 100644 src/app/Jobs/SyncFromGoogleCalendarJob.php create mode 100644 src/app/Listeners/GiveBonusCredits.php create mode 100644 src/app/Livewire/Activities/Index.php create mode 100644 src/app/Livewire/Admin/Affiliates/Index.php create mode 100644 src/app/Livewire/Admin/Dashboard.php create mode 100644 src/app/Livewire/Admin/Features/Modals/Form.php create mode 100644 src/app/Livewire/Admin/Plans/Index.php create mode 100644 src/app/Livewire/Admin/Plans/Modals/Form.php create mode 100644 src/app/Livewire/Admin/Translations/Index.php create mode 100644 src/app/Livewire/Admin/Translations/Modals/DeleteModal.php create mode 100644 src/app/Livewire/Admin/Translations/Modals/EditModal.php create mode 100644 src/app/Livewire/Admin/Users/Calendar.php create mode 100644 src/app/Livewire/Admin/Users/Detail.php create mode 100644 src/app/Livewire/Admin/Users/Index.php create mode 100644 src/app/Livewire/Admin/Versions.php create mode 100644 src/app/Livewire/Agent/History.php create mode 100644 src/app/Livewire/Agent/Index.php create mode 100644 src/app/Livewire/Agent/Logs.php create mode 100644 src/app/Livewire/Agent/Modals/ConflictModal.php create mode 100644 src/app/Livewire/Auth/ForgotPassword.php create mode 100644 src/app/Livewire/Auth/Login.php create mode 100644 src/app/Livewire/Auth/Modals/VerifyModal.php create mode 100644 src/app/Livewire/Auth/Register.php create mode 100644 src/app/Livewire/Auth/ResetPassword.php create mode 100644 src/app/Livewire/Auth/Verify.php create mode 100644 src/app/Livewire/Auth/VerifyNotice.php create mode 100644 src/app/Livewire/Automation/Index.php create mode 100644 src/app/Livewire/Calendar/Forms/EventForm.php create mode 100644 src/app/Livewire/Calendar/Index.php create mode 100644 src/app/Livewire/Calendar/Modals/DayEventsModal.php create mode 100644 src/app/Livewire/Calendar/Modals/EventModal.php create mode 100644 src/app/Livewire/Calendar/Sidebar.php create mode 100644 src/app/Livewire/Calendar/WeekCanvas.php create mode 100644 src/app/Livewire/Checkout/Index.php create mode 100644 src/app/Livewire/Checkout/Success.php create mode 100644 src/app/Livewire/Contacts/Index.php create mode 100644 src/app/Livewire/Dashboard/Index.php create mode 100644 src/app/Livewire/Homepage/Agb.php create mode 100644 src/app/Livewire/Homepage/Datenschutz.php create mode 100644 src/app/Livewire/Homepage/Example.php create mode 100644 src/app/Livewire/Homepage/Impressum.php create mode 100644 src/app/Livewire/Homepage/Index.php create mode 100644 src/app/Livewire/Homepage/Kontakt.php create mode 100644 src/app/Livewire/Homepage/Preise.php create mode 100644 src/app/Livewire/Homepage/UeberUns.php create mode 100644 src/app/Livewire/Integration/Index.php create mode 100644 src/app/Livewire/Integration/Modals/Form.php create mode 100644 src/app/Livewire/Invoices/Index.php create mode 100644 src/app/Livewire/Notes/Index.php create mode 100644 src/app/Livewire/Notifications/Bell.php create mode 100644 src/app/Livewire/Payments/Index.php create mode 100644 src/app/Livewire/Plans/Index.php create mode 100644 src/app/Livewire/Settings/Index.php create mode 100644 src/app/Livewire/Settings/Modals/DeleteAccount.php create mode 100644 src/app/Livewire/Settings/Modals/SmtpError.php create mode 100644 src/app/Livewire/Subscription/Index.php create mode 100644 src/app/Livewire/Tasks/Index.php create mode 100644 src/app/Mail/AffiliateQualifiedMail.php create mode 100644 src/app/Mail/AriaComposedMail.php create mode 100644 src/app/Mail/GiftAccessMail.php create mode 100644 src/app/Mail/ResetPasswordMail.php create mode 100644 src/app/Mail/SmtpTestMail.php create mode 100644 src/app/Models/Activity.php create mode 100644 src/app/Models/Affiliate.php create mode 100644 src/app/Models/AffiliateCreditLog.php create mode 100644 src/app/Models/AffiliateReferral.php create mode 100644 src/app/Models/AgentLog.php create mode 100644 src/app/Models/ApiToken.php create mode 100644 src/app/Models/AppVersion.php create mode 100644 src/app/Models/Automation.php create mode 100644 src/app/Models/CalendarIntegration.php create mode 100644 src/app/Models/Contact.php create mode 100644 src/app/Models/CreditTransaction.php create mode 100644 src/app/Models/Device.php create mode 100644 src/app/Models/EmailLog.php create mode 100644 src/app/Models/Event.php create mode 100644 src/app/Models/EventDurationStat.php create mode 100644 src/app/Models/EventKeyword.php create mode 100644 src/app/Models/Feature.php create mode 100644 src/app/Models/FeatureGroup.php create mode 100644 src/app/Models/MailQueue.php create mode 100644 src/app/Models/Note.php create mode 100644 src/app/Models/Notification.php create mode 100644 src/app/Models/Payment.php create mode 100644 src/app/Models/Plan.php create mode 100644 src/app/Models/Reminder.php create mode 100644 src/app/Models/Subscription.php create mode 100644 src/app/Models/Task.php create mode 100644 src/app/Models/Translation.php create mode 100644 src/app/Models/User.php create mode 100644 src/app/Models/Verification.php create mode 100644 src/app/Notifications/EventReminderNotification.php create mode 100644 src/app/Notifications/TaskReminderNotification.php create mode 100644 src/app/Observers/EventObserver.php create mode 100644 src/app/Providers/AppServiceProvider.php create mode 100644 src/app/Services/AgentAIService.php create mode 100644 src/app/Services/AgentActionService.php create mode 100644 src/app/Services/AgentContextService.php create mode 100644 src/app/Services/AgentParserService.php create mode 100644 src/app/Services/DurationLearningService.php create mode 100644 src/app/Services/EventPlannerService.php create mode 100644 src/app/Services/GoogleCalendarService.php create mode 100644 src/app/Services/MailService.php create mode 100644 src/app/Services/MailerService.php create mode 100644 src/app/Services/PushService.php create mode 100644 src/app/Services/StripeService.php create mode 100644 src/app/Services/VerificationService.php create mode 100644 src/app/View/Components/AppHeader.php create mode 100755 src/artisan create mode 100644 src/bootstrap/app.php create mode 100644 src/bootstrap/providers.php create mode 100644 src/composer.json create mode 100644 src/composer.lock create mode 100644 src/config/ai_models.php create mode 100644 src/config/app.php create mode 100644 src/config/auth.php create mode 100644 src/config/automations.php create mode 100644 src/config/broadcasting.php create mode 100644 src/config/cache.php create mode 100644 src/config/database.php create mode 100644 src/config/filesystems.php create mode 100644 src/config/livewire.php create mode 100644 src/config/logging.php create mode 100644 src/config/mail.php create mode 100644 src/config/queue.php create mode 100644 src/config/reverb.php create mode 100644 src/config/services.php create mode 100644 src/config/session.php create mode 100644 src/config/sidebar.php create mode 100644 src/config/wire-elements-modal.php create mode 100644 src/database/.gitignore create mode 100644 src/database/factories/UserFactory.php create mode 100644 src/database/migrations/0001_01_01_000000_create_users_table.php create mode 100644 src/database/migrations/0001_01_01_000001_create_cache_table.php create mode 100644 src/database/migrations/0001_01_01_000002_create_jobs_table.php create mode 100644 src/database/migrations/2026_04_03_223546_create_reminders_table.php create mode 100644 src/database/migrations/2026_04_03_223948_create_activities_table.php create mode 100644 src/database/migrations/2026_04_03_224035_create_contacts_table.php create mode 100644 src/database/migrations/2026_04_03_224117_create_devices_table.php create mode 100644 src/database/migrations/2026_04_03_231554_create_plans_table.php create mode 100644 src/database/migrations/2026_04_03_231605_create_subscriptions_table.php create mode 100644 src/database/migrations/2026_04_03_231620_create_payments_table.php create mode 100644 src/database/migrations/2026_04_04_003244_create_translations_table.php create mode 100644 src/database/migrations/2026_04_04_223341_create_mail_queues_table.php create mode 100644 src/database/migrations/2026_04_05_195243_create_verifications_table.php create mode 100644 src/database/migrations/2026_04_05_220804_create_agent_logs_table.php create mode 100644 src/database/migrations/2026_04_05_223224_create_events_table.php create mode 100644 src/database/migrations/2026_04_06_212207_create_feature_groups_table.php create mode 100644 src/database/migrations/2026_04_06_212208_create_features_table.php create mode 100644 src/database/migrations/2026_04_06_212250_create_feature_plan_table.php create mode 100644 src/database/migrations/2026_04_07_164352_create_event_duration_stats_table.php create mode 100644 src/database/migrations/2026_04_07_173346_create_event_keywords_table.php create mode 100644 src/database/migrations/2026_04_08_181424_create_event_user_table.php create mode 100644 src/database/migrations/2026_04_11_000001_create_calendar_integrations_table.php create mode 100644 src/database/migrations/2026_04_12_000001_create_automations_table.php create mode 100644 src/database/migrations/2026_04_12_100001_create_notes_table.php create mode 100644 src/database/migrations/2026_04_12_100002_create_tasks_table.php create mode 100644 src/database/migrations/2026_04_15_000002_create_affiliate_tables.php create mode 100644 src/database/migrations/2026_04_15_100001_create_credit_transactions_table.php create mode 100644 src/database/migrations/2026_04_15_200001_create_api_tokens_table.php create mode 100644 src/database/migrations/2026_04_17_100003_create_notifications_table.php create mode 100644 src/database/migrations/2026_04_17_100004_create_sent_reminders_table.php create mode 100644 src/database/migrations/2026_04_17_100005_create_email_logs_table.php create mode 100644 src/database/migrations/2026_04_18_000004_create_event_contact_table.php create mode 100644 src/database/migrations/2026_04_18_083033_alter_sent_reminders_nullable_event_and_type.php create mode 100644 src/database/migrations/2026_04_18_104656_add_reminder_at_to_tasks_table.php create mode 100644 src/database/migrations/2026_04_18_160645_create_versions_table.php create mode 100644 src/database/seeders/DatabaseSeeder.php create mode 100644 src/database/seeders/ErrorTranslationSeeder.php create mode 100644 src/database/seeders/FeatureGroupSeeder.php create mode 100644 src/database/seeders/FeatureSeeder.php create mode 100644 src/database/seeders/InternalPlanSeeder.php create mode 100644 src/database/seeders/MigrateBonusCreditsSeeder.php create mode 100644 src/database/seeders/PaymentSeeder.php create mode 100644 src/database/seeders/PlanSeeder.php create mode 100644 src/database/seeders/RolePlansSeeder.php create mode 100644 src/database/seeders/TranslationSeeder.php create mode 100644 src/package-lock.json create mode 100644 src/package.json create mode 100644 src/phpunit.xml create mode 100644 src/public/.htaccess create mode 100644 src/public/favicon.ico create mode 100644 src/public/images/logo-text.png create mode 100644 src/public/images/logo-white.png create mode 100644 src/public/images/logo.png create mode 100644 src/public/index.php create mode 100644 src/public/robots.txt create mode 100644 src/resources/css/app.css create mode 100644 src/resources/css/safelist-tailwindcss.txt create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-200.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-200italic.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-300.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-300italic.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-500.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-500italic.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-600.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-600italic.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-700.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-700italic.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-italic.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/bai-jamjuree-regular.woff2 create mode 100644 src/resources/fonts/BaiJamjuree/font.css create mode 100644 src/resources/fonts/Space/font.css create mode 100644 src/resources/fonts/Space/space age.otf create mode 100644 src/resources/fonts/Space/space age.ttf create mode 100644 src/resources/fonts/Syne/Syne-Bold.woff create mode 100644 src/resources/fonts/Syne/Syne-Bold.woff2 create mode 100644 src/resources/fonts/Syne/Syne-ExtraBold.woff create mode 100644 src/resources/fonts/Syne/Syne-ExtraBold.woff2 create mode 100644 src/resources/fonts/Syne/Syne-Italic.woff create mode 100644 src/resources/fonts/Syne/Syne-Italic.woff2 create mode 100644 src/resources/fonts/Syne/Syne-Regular.woff create mode 100644 src/resources/fonts/Syne/Syne-Regular.woff2 create mode 100644 src/resources/fonts/Syne/SyneMono-Regular.woff create mode 100644 src/resources/fonts/Syne/SyneMono-Regular.woff2 create mode 100644 src/resources/fonts/Syne/font.css create mode 100644 src/resources/js/agent-voice-orb.js create mode 100644 src/resources/js/app.js create mode 100644 src/resources/js/bootstrap.js create mode 100644 src/resources/js/calendar-interact.js create mode 100644 src/resources/js/calendar-week-canvas.js create mode 100644 src/resources/js/websocket/connection.js create mode 100644 src/resources/plugins/Notification/index.js create mode 100644 src/resources/plugins/Notification/src/config.js create mode 100644 src/resources/plugins/Notification/src/message.css create mode 100644 src/resources/plugins/Notification/src/message.js create mode 100644 src/resources/views/components/app-header.blade.php create mode 100644 src/resources/views/components/header.blade.php create mode 100644 src/resources/views/components/logo.blade.php create mode 100644 src/resources/views/components/sidebar.blade.php create mode 100644 src/resources/views/components/sidebar/link.blade.php create mode 100644 src/resources/views/emails/affiliate-qualified.blade.php create mode 100644 src/resources/views/emails/agent/message.blade.php create mode 100644 src/resources/views/emails/agent/reminder.blade.php create mode 100644 src/resources/views/emails/aria-composed.blade.php create mode 100644 src/resources/views/emails/auth/verify.blade.php create mode 100644 src/resources/views/emails/components/button.blade.php create mode 100644 src/resources/views/emails/components/code.blade.php create mode 100644 src/resources/views/emails/components/footer.blade.php create mode 100644 src/resources/views/emails/components/header.blade.php create mode 100644 src/resources/views/emails/gift-access.blade.php create mode 100644 src/resources/views/emails/layout.blade.php create mode 100644 src/resources/views/emails/reset-password.blade.php create mode 100644 src/resources/views/emails/smtp-test.blade.php create mode 100644 src/resources/views/errors/400.blade.php create mode 100644 src/resources/views/errors/401.blade.php create mode 100644 src/resources/views/errors/403.blade.php create mode 100644 src/resources/views/errors/404.blade.php create mode 100644 src/resources/views/errors/500.blade.php create mode 100644 src/resources/views/errors/501.blade.php create mode 100644 src/resources/views/errors/502.blade.php create mode 100644 src/resources/views/layouts/app.blade.php create mode 100644 src/resources/views/layouts/blank.blade.php create mode 100644 src/resources/views/livewire/activities/index.blade.php create mode 100644 src/resources/views/livewire/admin/affiliates/index.blade.php create mode 100644 src/resources/views/livewire/admin/dashboard.blade.php create mode 100644 src/resources/views/livewire/admin/features/modals/form.blade.php create mode 100644 src/resources/views/livewire/admin/plans/index.blade.php create mode 100644 src/resources/views/livewire/admin/plans/modals/form.blade.php create mode 100644 src/resources/views/livewire/admin/translations/index.blade.php create mode 100644 src/resources/views/livewire/admin/translations/modals/delete-modal.blade.php create mode 100644 src/resources/views/livewire/admin/translations/modals/edit-modal.blade.php create mode 100644 src/resources/views/livewire/admin/users/calendar.blade.php create mode 100644 src/resources/views/livewire/admin/users/detail.blade.php create mode 100644 src/resources/views/livewire/admin/users/index.blade.php create mode 100644 src/resources/views/livewire/admin/versions.blade.php create mode 100644 src/resources/views/livewire/agent/history.blade.php create mode 100644 src/resources/views/livewire/agent/index.blade.php create mode 100644 src/resources/views/livewire/agent/logs.blade.php create mode 100644 src/resources/views/livewire/agent/modals/conflict-modal.blade.php create mode 100644 src/resources/views/livewire/auth/forgot-password.blade.php create mode 100644 src/resources/views/livewire/auth/login.blade.php create mode 100644 src/resources/views/livewire/auth/modals/verify-modal.blade.php create mode 100644 src/resources/views/livewire/auth/register.blade.php create mode 100644 src/resources/views/livewire/auth/reset-password.blade.php create mode 100644 src/resources/views/livewire/auth/verify-notice.blade.php create mode 100644 src/resources/views/livewire/auth/verify.blade.php create mode 100644 src/resources/views/livewire/automation/index.blade.php create mode 100644 src/resources/views/livewire/calendar/day-canvas.blade.php create mode 100644 src/resources/views/livewire/calendar/forms/event-form.blade.php create mode 100644 src/resources/views/livewire/calendar/index.blade.php create mode 100644 src/resources/views/livewire/calendar/modals/day-events-modal.blade.php create mode 100644 src/resources/views/livewire/calendar/modals/event-modal.blade.php create mode 100644 src/resources/views/livewire/calendar/month-canvas.blade.php create mode 100644 src/resources/views/livewire/calendar/sidebar.blade.php create mode 100644 src/resources/views/livewire/calendar/skeletons/day.blade.php create mode 100644 src/resources/views/livewire/calendar/skeletons/month.blade.php create mode 100644 src/resources/views/livewire/calendar/skeletons/week.blade.php create mode 100644 src/resources/views/livewire/calendar/views/day-canvas.blade.php create mode 100644 src/resources/views/livewire/calendar/views/month-canvas.blade.php create mode 100644 src/resources/views/livewire/calendar/views/week-canvas.blade.php create mode 100644 src/resources/views/livewire/calendar/views_alt/day.blade.php create mode 100644 src/resources/views/livewire/calendar/views_alt/month.blade.php create mode 100644 src/resources/views/livewire/calendar/views_alt/week.blade.php create mode 100644 src/resources/views/livewire/calendar/week-canvas.blade.php create mode 100644 src/resources/views/livewire/checkout/index.blade.php create mode 100644 src/resources/views/livewire/checkout/success.blade.php create mode 100644 src/resources/views/livewire/contacts/index.blade.php create mode 100644 src/resources/views/livewire/dashboard/index.blade.php create mode 100644 src/resources/views/livewire/homepage/agb.blade.php create mode 100644 src/resources/views/livewire/homepage/datenschutz.blade.php create mode 100644 src/resources/views/livewire/homepage/example.blade.php create mode 100644 src/resources/views/livewire/homepage/impressum.blade.php create mode 100644 src/resources/views/livewire/homepage/index.blade.php create mode 100644 src/resources/views/livewire/homepage/kontakt.blade.php create mode 100644 src/resources/views/livewire/homepage/preise.blade.php create mode 100644 src/resources/views/livewire/homepage/ueber-uns.blade.php create mode 100644 src/resources/views/livewire/integration/index.blade.php create mode 100644 src/resources/views/livewire/integration/modals/form.blade.php create mode 100644 src/resources/views/livewire/integration/modals/provider.blade.php create mode 100644 src/resources/views/livewire/invoices/index.blade.php create mode 100644 src/resources/views/livewire/notes/index.blade.php create mode 100644 src/resources/views/livewire/notifications/bell.blade.php create mode 100644 src/resources/views/livewire/payments/index.blade.php create mode 100644 src/resources/views/livewire/plans/index.blade.php create mode 100644 src/resources/views/livewire/settings/index.blade.php create mode 100644 src/resources/views/livewire/settings/modals/delete-account.blade.php create mode 100644 src/resources/views/livewire/settings/modals/smtp-error.blade.php create mode 100644 src/resources/views/livewire/subscription/index.blade.php create mode 100644 src/resources/views/livewire/tasks/index.blade.php create mode 100644 src/resources/views/partials/homepage/footer.blade.php create mode 100644 src/resources/views/partials/homepage/navbar.blade.php create mode 100644 src/resources/views/vendor/wire-elements-modal/modal.blade.php create mode 100644 src/resources/views/welcome.blade.php create mode 100644 src/routes/api.php create mode 100644 src/routes/channels.php create mode 100644 src/routes/connect.php create mode 100644 src/routes/console.php create mode 100644 src/routes/web.php create mode 100644 src/routes/www.php create mode 100644 src/storage/app/.gitignore create mode 100644 src/storage/app/private/.gitignore create mode 100644 src/storage/app/public/.gitignore create mode 100644 src/storage/framework/.gitignore create mode 100644 src/storage/framework/testing/.gitignore create mode 100644 src/tests/Feature/AutomationTest.php create mode 100644 src/tests/Feature/ExampleTest.php create mode 100644 src/tests/TestCase.php create mode 100644 src/tests/Unit/ExampleTest.php create mode 100644 src/vite.config.js diff --git a/.env b/.env new file mode 100644 index 0000000..3991602 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +DB_CONNECTION=mysql +DB_HOST=db +DB_DATABASE=nexxo +DB_USERNAME=nexxo +DB_PASSWORD=5d79bcb6f4ce1955ef835af6 +DB_ROOT_PASSWORD=49f12736549babe3a2638078b95b0407 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0531d75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +src/.env +src/.env.local +src/.env.development +src/.env.staging +src/.env.production +src/vendor/ +src/node_modules/ +src/public/build/ +src/storage/logs/ +src/storage/framework/cache/ +src/storage/framework/sessions/ +src/storage/framework/views/ +src/bootstrap/cache/ +db_data/ +*.log diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..6f91bf4 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +echo "πŸš€ Aziros deploying..." + +cd ~/aziros + +# Migrations ausfΓΌhren +docker compose exec app php artisan migrate --force + +# Cache leeren +docker compose exec app php artisan config:clear +docker compose exec app php artisan cache:clear +docker compose exec app php artisan view:clear +docker compose exec app php artisan route:clear + +# Caches neu aufbauen +docker compose exec app php artisan config:cache +docker compose exec app php artisan route:cache +docker compose exec app php artisan view:cache + +echo "βœ… Deploy fertig!" \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..01f0c11 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,169 @@ +services: + db: + networks: + - nexxo + image: mariadb:11 + container_name: nexxo_db + restart: unless-stopped + env_file: .env + environment: + MARIADB_DATABASE: ${DB_DATABASE} + MARIADB_USER: ${DB_USERNAME} + MARIADB_PASSWORD: ${DB_PASSWORD} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + ports: + - "3306:3306" + volumes: + - ./db_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$MARIADB_ROOT_PASSWORD || exit 1"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 20s + + app: + networks: + - nexxo + build: + context: . + dockerfile: docker/php/Dockerfile + args: + UID: 1000 + GID: 1000 + image: nexxo-php + container_name: nexxo_app + ports: + - "5173:5173" + restart: unless-stopped + working_dir: /var/www/html + volumes: + - ./src:/var/www/html + env_file: src/.env.development + environment: + DB_HOST: db + REDIS_HOST: redis + XDG_CONFIG_HOME: /tmp + PSYSH_CONFIG_DIR: /tmp/psysh + depends_on: + db: + condition: service_healthy + + web: + networks: + - nexxo + image: nginx:alpine + container_name: nexxo_web + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./src:/var/www/html:ro + - ./docker/nginx/local.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - app + + redis: + networks: + - nexxo + image: redis:7-alpine + container_name: nexxo_redis + restart: unless-stopped + ports: + - "127.0.0.1:6379:6379" + command: ["redis-server", "--appendonly", "yes"] + + mail-worker: + networks: + - nexxo + image: nexxo-php + container_name: nexxo_mail_worker + restart: unless-stopped + env_file: src/.env.development + volumes: + - ./src:/var/www/html + working_dir: /var/www/html + command: php artisan app:process-mail-queue + depends_on: + db: + condition: service_healthy + + worker: + networks: + - nexxo + image: nexxo-php + container_name: nexxo_worker + restart: unless-stopped + env_file: src/.env.development + volumes: + - ./src:/var/www/html + working_dir: /var/www/html + command: php artisan queue:work --sleep=1 --tries=3 --timeout=120 + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + + scheduler: + image: nexxo-php + container_name: nexxo_scheduler + restart: unless-stopped + networks: + - nexxo + volumes: + - ./src:/var/www/html + env_file: src/.env.development + environment: + DB_HOST: db + REDIS_HOST: redis + command: > + sh -c ' + until nc -z db 3306; do sleep 1; done; + php artisan schedule:work + ' + depends_on: + - db + + reverb: + networks: + - nexxo + image: nexxo-php + container_name: nexxo_reverb + command: php artisan reverb:start --host=0.0.0.0 --port=8080 + env_file: src/.env.development + ports: + - "8080:8080" + restart: unless-stopped + volumes: + - ./src:/var/www/html + depends_on: + - redis + + ntfy: + image: binwiederhier/ntfy + container_name: nexxo_ntfy + restart: unless-stopped + ports: + - "8082:80" + volumes: + - ./docker/ntfy/cache:/var/cache/ntfy + - ./docker/ntfy/config:/etc/ntfy + command: + - serve + + + horizon: + networks: + - nexxo + image: nexxo-php + container_name: nexxo_horizon + command: php artisan horizon + volumes: + - ./src:/var/www/html + depends_on: + - redis + +networks: + nexxo: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..84fb344 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,166 @@ +services: + db: + networks: + - nexxo + image: mariadb:11 + container_name: nexxo_db + restart: unless-stopped + env_file: .env + environment: + MARIADB_DATABASE: ${DB_DATABASE} + MARIADB_USER: ${DB_USERNAME} + MARIADB_PASSWORD: ${DB_PASSWORD} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + ports: + - "3306:3306" + volumes: + - ./db_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u root -p$$MARIADB_ROOT_PASSWORD || exit 1"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 20s + + app: + networks: + - nexxo + build: + context: . + dockerfile: docker/php/Dockerfile + args: + UID: 1000 + GID: 1000 + image: nexxo-php + container_name: nexxo_app + ports: + - "5173:5173" + restart: unless-stopped + working_dir: /var/www/html + volumes: + - ./src:/var/www/html + environment: + DB_HOST: db + REDIS_HOST: redis + XDG_CONFIG_HOME: /tmp + PSYSH_CONFIG_DIR: /tmp/psysh + depends_on: + db: + condition: service_healthy + + web: + networks: + - nexxo + image: nginx:alpine + container_name: nexxo_web + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./src:/var/www/html:ro + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - app + + redis: + networks: + - nexxo + image: redis:7-alpine + container_name: nexxo_redis + restart: unless-stopped + ports: + - "127.0.0.1:6379:6379" + command: ["redis-server", "--appendonly", "yes"] + + mail-worker: + networks: + - nexxo + image: nexxo-php + container_name: nexxo_mail_worker + restart: unless-stopped + env_file: .env # πŸ”₯ DAS HIER + volumes: + - ./src:/var/www/html + working_dir: /var/www/html + command: php artisan app:process-mail-queue + depends_on: + db: + condition: service_healthy + + worker: + networks: + - nexxo + image: nexxo-php + container_name: nexxo_worker + restart: unless-stopped + env_file: .env # πŸ”₯ auch hier + volumes: + - ./src:/var/www/html + working_dir: /var/www/html + command: php artisan queue:work --sleep=1 --tries=3 --timeout=120 + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + + scheduler: + image: nexxo-php + container_name: nexxo_scheduler + restart: unless-stopped + networks: + - nexxo + volumes: + - ./src:/var/www/html + environment: + DB_HOST: db + REDIS_HOST: redis + command: > + sh -c ' + until nc -z db 3306; do sleep 1; done; + php artisan schedule:work + ' + depends_on: + - db + + reverb: + networks: + - nexxo + image: nexxo-php + container_name: nexxo_reverb + command: php artisan reverb:start --host=0.0.0.0 --port=8080 + ports: + - "8080:8080" + restart: unless-stopped + volumes: + - ./src:/var/www/html + depends_on: + - redis + + ntfy: + image: binwiederhier/ntfy + container_name: nexxo_ntfy + restart: unless-stopped + ports: + - "8082:80" + volumes: + - ./docker/ntfy/cache:/var/cache/ntfy + - ./docker/ntfy/config:/etc/ntfy + command: + - serve + + + horizon: + networks: + - nexxo + image: nexxo-php + container_name: nexxo_horizon + command: php artisan horizon + volumes: + - ./src:/var/www/html + depends_on: + - redis + +networks: + nexxo: + driver: bridge \ No newline at end of file diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..e01fb8b --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,54 @@ +server { + listen 80; + server_name app.aziros.com connect.aziros.com api.aziros.com www.aziros.com socket.aziros.com 10.10.90.102; + + root /var/www/html/public; + index index.php index.html; + + client_max_body_size 20M; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Hauptrouting (Laravel) + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ^~ /livewire { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP Handling + location ~ \.php$ { + try_files $uri =404; + + fastcgi_pass app:9000; + fastcgi_index index.php; + + include fastcgi_params; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + + fastcgi_param HTTPS on; + fastcgi_param HTTP_X_FORWARDED_PROTO https; + } + + # Security + location ~ /\.ht { + deny all; + } + + # Optional: favicon / robots + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + +} + +# Default Block (alles andere droppen) +server { + listen 80 default_server; + server_name _; + return 444; +} diff --git a/docker/nginx/local.conf b/docker/nginx/local.conf new file mode 100644 index 0000000..c879087 --- /dev/null +++ b/docker/nginx/local.conf @@ -0,0 +1,58 @@ +server { + listen 80; + server_name app.aziros.local + api.aziros.local + socket.aziros.local + vite.aziros.local + 10.10.90.102; + + root /var/www/html/public; + index index.php index.html; + + client_max_body_size 20M; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Hauptrouting (Laravel) + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ^~ /livewire { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP Handling + location ~ \.php$ { + try_files $uri =404; + + fastcgi_pass app:9000; + fastcgi_index index.php; + + include fastcgi_params; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + + fastcgi_param HTTPS off; + fastcgi_param HTTP_X_FORWARDED_PROTO http; + } + + # Security + location ~ /\.ht { + deny all; + } + + # Optional: favicon / robots + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + +} + +# Default Block (alles andere droppen) +server { + listen 80 default_server; + server_name _; + return 444; +} diff --git a/docker/ntfy/cache/server.yml b/docker/ntfy/cache/server.yml new file mode 100644 index 0000000..ed689b3 --- /dev/null +++ b/docker/ntfy/cache/server.yml @@ -0,0 +1,17 @@ +base-url: "https://notify.aziros.com" +listen-http: ":80" +web-root: "disable" + +auth-file: "/etc/ntfy/auth.db" +auth-default-access: deny-all + +cache-file: "/var/cache/ntfy/cache.db" +cache-duration: "24h" + +behind-proxy: true + +upstream-base-url: "https://ntfy.sh" + +#auth-access: +# - "nexxo:*:write" +# docker exec -it nexxo_ntfy ntfy user add USERNAME diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..f7d4a56 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,60 @@ +FROM php:8.3-fpm + +# ---------- ARG fΓΌr UID/GID ---------- +ARG UID=1000 +ARG GID=1000 + +# ---------- Systempakete ---------- +RUN apt-get update && apt-get install -y \ + git unzip libzip-dev libicu-dev libpng-dev libonig-dev libxml2-dev \ + libjpeg62-turbo-dev libfreetype6-dev libwebp-dev libssl-dev locales rsync ffmpeg \ + nmap iproute2 net-tools netcat-openbsd curl \ + && docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \ + && docker-php-ext-install -j"$(nproc)" intl zip pdo_mysql gd bcmath opcache pcntl exif \ + && rm -rf /var/lib/apt/lists/* + +# ---------- Node.js ---------- +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs + +# ---------- Redis Extension ---------- +RUN pecl install redis && docker-php-ext-enable redis + +# ---------- Composer ---------- +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# ---------- PHP Config ---------- +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ + && echo "memory_limit=512M" >> /usr/local/etc/php/conf.d/memory.ini \ + && echo "upload_max_filesize=20M" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "post_max_size=20M" >> /usr/local/etc/php/conf.d/uploads.ini + +# ---------- PHP-FPM ---------- +RUN mkdir -p /usr/local/etc/php-fpm.d \ + && echo "[www]" > /usr/local/etc/php-fpm.d/www.conf \ + && echo "user = www-data" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "group = www-data" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "listen = 9000" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "pm = dynamic" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "pm.max_children = 16" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "pm.start_servers = 4" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "pm.min_spare_servers = 4" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "pm.max_spare_servers = 8" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "pm.max_requests = 500" >> /usr/local/etc/php-fpm.d/www.conf + +# ---------- UID/GID Fix ---------- +RUN usermod -u ${UID} www-data \ + && groupmod -g ${GID} www-data + +# ---------- Working Dir ---------- +WORKDIR /var/www/html + +# ---------- Cache Ordner ---------- +RUN mkdir -p /var/www/.cache \ + /var/www/.npm + +# ---------- Rechte (nur Cache!) ---------- +RUN chown -R www-data:www-data /var/www/.cache /var/www/.npm + +# ---------- User ---------- +USER www-data \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/src/.env.example b/src/.env.example new file mode 100644 index 0000000..c3217b9 --- /dev/null +++ b/src/.env.example @@ -0,0 +1,130 @@ +APP_NAME="aziros GmbH" +APP_ENV=local +# Generieren mit: php artisan key:generate +APP_KEY= +APP_DEBUG=true +APP_URL=http://app.aziros.local +APP_CONNECT_URL=http://connect.aziros.local +ASSET_URL=http://app.aziros.local +LIVEWIRE_ASSET_URL=http://app.aziros.local + +APP_VERSION=1.0.0 + +DOMAIN_APP=app.aziros.local +DOMAIN_API=api.aziros.local +DOMAIN_CONNECT=connect.aziros.local +DOMAIN_WWW=www.aziros.local + +APP_LOCALE=de +APP_FALLBACK_LOCALE=de +APP_FAKER_LOCALE=de_DE + +APP_MAINTENANCE_DRIVER=file + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=daily +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=nexxo +DB_USERNAME=nexxo +DB_PASSWORD= + +SESSION_DRIVER=redis +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=.aziros.local +SESSION_SECURE_COOKIE=false + +BROADCAST_CONNECTION=reverb +FILESYSTEM_DISK=local +QUEUE_CONNECTION=redis + +CACHE_STORE=redis +CACHE_PREFIX=az_cache: +CACHE_DRIVER=redis + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=redis +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST= +MAIL_PORT=587 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" + +BROADCAST_DRIVER=reverb + +REVERB_SERVER_PORT=8080 +REVERB_SERVER_HOST=0.0.0.0 + +REVERB_APP_ID= +REVERB_APP_KEY= +# Generieren mit: php artisan reverb:install +REVERB_APP_SECRET= + +REVERB_HOST=reverb +REVERB_PORT=8080 +REVERB_SCHEME=http + +VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" +VITE_REVERB_HOST=socket.aziros.local +VITE_REVERB_PORT=80 +VITE_REVERB_SCHEME=ws + +# Von https://dashboard.stripe.com/apikeys +STRIPE_KEY= +STRIPE_SECRET= +STRIPE_WEBHOOK_SECRET= +STRIPE_CURRENCY=eur + +# Von https://platform.openai.com/api-keys +OPENAI_API_KEY= +OPENAI_MODEL=gpt-4.1 + +# Von https://console.cloud.google.com/auth/clients?project=aziros +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CLIENT_API= +GOOGLE_REDIRECT_URI="http://connect.aziros.local/integrations/google/callback" +GOOGLE_CALENDAR_WEBHOOK_URL="http://api.aziros.local/webhooks/google-calendar" + +# Von https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps +MICROSOFT_CLIENT_ID= +MICROSOFT_CLIENT_SECRET= +MICROSOFT_TENANT= +MICROSOFT_REDIRECT_URI="http://connect.aziros.local/integrations/outlook/callback" + +MAIL_REMINDER_USERNAME= +MAIL_REMINDER_PASSWORD= + +MAIL_SYSTEM_USERNAME= +MAIL_SYSTEM_PASSWORD= + +MAIL_ARIA_USERNAME= +MAIL_ARIA_PASSWORD= + +MAIL_HELLO_USERNAME= +MAIL_HELLO_PASSWORD= + +EXPO_TOKEN= diff --git a/src/.gitattributes b/src/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/src/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..0eeb578 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,25 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +_ide_helper.php +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/src/CLAUDE.md b/src/CLAUDE.md new file mode 100644 index 0000000..e6055a3 --- /dev/null +++ b/src/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Development +npm run dev # Vite dev server (hot-reload) +php artisan serve # Laravel dev server + +# Build +npm run build # Production Vite build + +# Database +php artisan migrate +php artisan migrate:fresh --seed + +# Testing +php artisan test +php artisan test --filter=TestName + +# Livewire +php artisan livewire:make ComponentName +``` + +## Architecture + +**Stack:** Laravel 13 + Livewire 4 + Alpine.js + Tailwind CSS 4 + Vite + Laravel Reverb (WebSockets) + +### Livewire Components (`app/Livewire/`) +Each feature lives in its own namespace folder with a matching `resources/views/livewire/` blade. Components use `.layout('layouts.app')` in their `render()` method. + +| Namespace | Route | Purpose | +|-----------|-------|---------| +| `Calendar/WeekCanvas` | `/calendar` | Main calendar view (week/day/month) | +| `Calendar/Sidebar` | β€” | Slide-in event create/edit panel | +| `Activities/Index` | `/activities` | Activity log with search, filter, pagination | +| `Automation/Index` | `/automations` | Automation rules with slide-in config panel | +| `Integration/Index` | `/integrations` | Google Calendar + Outlook OAuth integration cards | +| `Settings/Index` | `/settings` | Tab-based settings (profile, security, notifications, SMTP) | +| `Contacts/Index` | `/contacts` | Contact management | +| `Agent/Index` | `/agent` | AI agent interface | + +### Models (`app/Models/`) +- **User** β€” has `settings` (json column), `timezone`, `locale`, `hasFeature(string $key)` helper +- **Event** β€” calendar events with color (hex), recurrence, user-owned +- **Activity** β€” audit log; use `Activity::log($userId, $type, $title, ...)` static factory; `visual()` returns icon/bg/text; `typeLabel()` returns German label +- **Automation** β€” user automations with `config` (json); `cfg($key, $default)` helper +- **CalendarIntegration** β€” Google/Outlook OAuth tokens; unique on `[user_id, provider]` +- **Reminder**, **Contact**, **Plan**, **Feature**, **Subscription** β€” supporting models + +### Plan / Feature Gating +`User::hasFeature('feature_key')` checks the user's active subscription plan against the `features` pivot. Used for Pro-only functionality (e.g., sync modes in integrations). + +### Calendar Views +The main calendar is `WeekCanvas.php` / `week-canvas.blade.php`. It renders week, day, and month views with: +- `resources/js/calendar-week-canvas.js` β€” Alpine.js component handling drag, resize, dblclick-to-create +- `resources/js/calendar-interact.js` β€” interact.js integration for resizing +- Overlapping event layout: greedy depth/column assignment in the blade's `@php` block +- Solid pastel backgrounds: `rgb(255-(255-r)*0.18, ...)` formula (not transparent rgba) + +### OAuth Integration Flow +`app/Http/Controllers/Integration/` β€” full-page redirect controllers (not Livewire). After callback, redirect to `/integrations?connected=google`. The Livewire Integration component reads the `?connected` query param on mount to show success state. + +### Event System (Livewire ↔ Alpine) +- `$this->dispatch('sidebar:createEvent', date: ..., time: ...)` β€” opens sidebar +- `$this->dispatch('eventCreated')` β€” calendar refreshes via `$listeners = ['eventCreated' => '$refresh']` +- `$this->dispatch('notify', ['type' => 'success', 'message' => '...'])` β€” toast notifications +- `$this->dispatch('openModal', 'component.name', [...])` β€” wire-elements/modal + +### Translations +`t('key')` is a custom helper (wraps `__()`) used throughout. Language files in `resources/lang/`. Supported locales: `de`, `en` (configured in `config/app.php` under `locales`). + +### Design System +Consistent Tailwind patterns across all views: +- Cards: `bg-white border border-gray-100 rounded-xl shadow-sm` +- Primary color: `indigo-600` / `bg-indigo-50` +- Section headings: `text-xl font-semibold text-gray-800` +- Badges/chips: `text-[10px] font-medium px-1.5 py-0.5 rounded-md` +- Danger: `text-red-500 hover:text-red-600` + +### Authentication +Custom auth middleware: `auth.custom` (guests), `guest.custom` (guests), `user` (authenticated users). Not using Laravel's default `auth` middleware. diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..5ad1377 --- /dev/null +++ b/src/README.md @@ -0,0 +1,58 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals. + +## Agentic Development + +Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow: + +```bash +composer require laravel/boost --dev + +php artisan boost:install +``` + +Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices. + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/src/app/Console/Commands/CheckExpiredSubscriptions.php b/src/app/Console/Commands/CheckExpiredSubscriptions.php new file mode 100644 index 0000000..4600c89 --- /dev/null +++ b/src/app/Console/Commands/CheckExpiredSubscriptions.php @@ -0,0 +1,49 @@ +value, + SubscriptionStatus::Gifted->value, + ]) + ->whereNotNull('ends_at') + ->where('ends_at', '<', now()) + ->with('user') + ->get(); + + $freePlan = Plan::freePlan(); + + foreach ($expired as $sub) { + $sub->update(['status' => SubscriptionStatus::Expired->value]); + + if ($freePlan && $sub->user) { + $sub->user->subscriptions()->create([ + 'plan_id' => $freePlan->id, + 'plan_name' => $freePlan->name, + 'plan_slug' => $freePlan->plan_key ?? str($freePlan->name)->slug(), + 'price' => 0, + 'interval' => 'monthly', + 'status' => SubscriptionStatus::Active->value, + 'starts_at' => now(), + 'ends_at' => null, + ]); + } + + $this->info("Subscription von {$sub->user?->name} abgelaufen."); + } + + $this->info("{$expired->count()} Subscriptions deaktiviert."); + } +} diff --git a/src/app/Console/Commands/CleanMailQueue.php b/src/app/Console/Commands/CleanMailQueue.php new file mode 100644 index 0000000..eaf6f7f --- /dev/null +++ b/src/app/Console/Commands/CleanMailQueue.php @@ -0,0 +1,48 @@ +where('sent_at', '<', now()->subDay()) + ->delete(); + + // πŸ”₯ FAILED (7 Tage) + $failed = MailQueue::where('status', 'failed') + ->where('updated_at', '<', now()->subDays(7)) + ->delete(); + + // πŸ”₯ CANCELED (2 Tage) + $canceled = MailQueue::where('status', 'canceled') + ->where('updated_at', '<', now()->subDays(2)) + ->delete(); + + // πŸ”₯ STUCK PROCESSING β†’ zurΓΌcksetzen + $recovered = MailQueue::where('status', 'processing') + ->where('updated_at', '<', now()->subMinutes(10)) + ->update([ + 'status' => 'pending', + 'available_at' => now(), + ]); + + + $this->info("Sent deleted: $sent"); + $this->info("Failed deleted: $failed"); + $this->info("Canceled deleted: $canceled"); + $this->info("Recovered jobs: $recovered"); + } +} diff --git a/src/app/Console/Commands/CleanupDeletedUsers.php b/src/app/Console/Commands/CleanupDeletedUsers.php new file mode 100644 index 0000000..0848a70 --- /dev/null +++ b/src/app/Console/Commands/CleanupDeletedUsers.php @@ -0,0 +1,33 @@ +where('deleted_at', '<=', now()->subDays(7)) + ->chunk(100, function ($users) { + + foreach ($users as $user) { + $user->anonymize(); + } + }); + + Verification::whereNotNull('verified_at') + ->orWhere('expires_at', '<', now()) + ->delete(); + } +} diff --git a/src/app/Console/Commands/ConsolidateSubscriptions.php b/src/app/Console/Commands/ConsolidateSubscriptions.php new file mode 100644 index 0000000..ea35fe5 --- /dev/null +++ b/src/app/Console/Commands/ConsolidateSubscriptions.php @@ -0,0 +1,81 @@ +option('dry-run'); + + $freePlan = Plan::freePlan(); + + // Alle User mit mehr als einer Subscription + $userIds = Subscription::select('user_id') + ->groupBy('user_id') + ->havingRaw('COUNT(*) > 1') + ->pluck('user_id'); + + if ($userIds->isEmpty()) { + $this->info('Keine doppelten Subscriptions gefunden.'); + return self::SUCCESS; + } + + $this->info("Betroffene User: {$userIds->count()}"); + + foreach ($userIds as $userId) { + $subs = Subscription::where('user_id', $userId) + ->orderByRaw("CASE WHEN status = 'active' THEN 0 ELSE 1 END") + ->orderByRaw("CASE WHEN provider = 'stripe' THEN 0 ELSE 1 END") + ->orderBy('created_at', 'desc') + ->get(); + + // Zu behaltende Subscription: aktive Stripe-Sub bevorzugt, sonst neuste aktive, sonst neuste + $keep = $subs->first(); + + // provider_customer_id aus einer beliebigen Sub retten (auch wenn keep sie nicht hat) + $customerId = $subs->whereNotNull('provider_customer_id')->first()?->provider_customer_id; + + $discard = $subs->where('id', '!=', $keep->id); + + $this->line("User {$userId}: behalte {$keep->id} ({$keep->plan_slug}/{$keep->status}), lΓΆsche {$discard->count()} Zeile(n)"); + + if ($dryRun) continue; + + DB::transaction(function () use ($keep, $discard, $customerId) { + // Payments auf die verbleibende Subscription umhΓ€ngen + foreach ($discard as $sub) { + Payment::where('subscription_id', $sub->id) + ->update(['subscription_id' => $keep->id]); + } + + // ÜberschΓΌssige Zeilen lΓΆschen + Subscription::whereIn('id', $discard->pluck('id'))->delete(); + + // customer_id sichern falls keep sie nicht hatte + if (!$keep->provider_customer_id && $customerId) { + $keep->update(['provider_customer_id' => $customerId]); + } + }); + } + + if (!$dryRun) { + $this->info('Konsolidierung abgeschlossen.'); + } else { + $this->warn('Dry-run – keine Γ„nderungen geschrieben. Ohne --dry-run ausfΓΌhren zum Anwenden.'); + } + + return self::SUCCESS; + } +} diff --git a/src/app/Console/Commands/CreateMissingAffiliates.php b/src/app/Console/Commands/CreateMissingAffiliates.php new file mode 100644 index 0000000..eb2fc3d --- /dev/null +++ b/src/app/Console/Commands/CreateMissingAffiliates.php @@ -0,0 +1,30 @@ +get(); + + foreach ($users as $user) { + Affiliate::create([ + 'user_id' => $user->id, + 'code' => Affiliate::generateCode($user), + 'status' => 'active', + ]); + + $this->info("Affiliate erstellt fΓΌr {$user->name}: " . $user->affiliate->code); + } + + $this->info("{$users->count()} Affiliate-Accounts erstellt."); + } +} diff --git a/src/app/Console/Commands/ProcessAffiliateQualifications.php b/src/app/Console/Commands/ProcessAffiliateQualifications.php new file mode 100644 index 0000000..2d1c493 --- /dev/null +++ b/src/app/Console/Commands/ProcessAffiliateQualifications.php @@ -0,0 +1,80 @@ +where('qualifies_at', '<=', now()) + ->with(['affiliate.user', 'referredUser']) + ->get(); + + foreach ($qualified as $referral) { + $referredUser = $referral->referredUser; + if (!$referredUser || $referredUser->status->value !== 'active') { + $referral->update(['status' => 'cancelled']); + continue; + } + + $affiliate = $referral->affiliate; + $credits = self::CREDITS_PER_REFERRAL; + + // Credits via Transaktion gutschreiben + CreditTransaction::affiliate( + $affiliate->user, + $credits, + $referredUser->name, + $referral->id + ); + + // Log erstellen + AffiliateCreditLog::create([ + 'affiliate_id' => $affiliate->id, + 'referral_id' => $referral->id, + 'credits' => $credits, + 'reason' => '3-Monats-Qualifikation: ' . $referredUser->name, + ]); + + // Referral als bezahlt markieren + $referral->update([ + 'status' => 'paid', + 'paid_at' => now(), + 'credits_awarded' => $credits, + ]); + + // Affiliate-Stats aktualisieren + $affiliate->increment('qualified_referrals'); + $affiliate->increment('total_credits_earned', $credits); + + // Benachrichtigung + try { + Mail::to($affiliate->user->email)->send( + new \App\Mail\AffiliateQualifiedMail( + $affiliate->user, + $referredUser, + $credits + ) + ); + } catch (\Throwable $e) { + report($e); + } + + $this->info("{$credits} Credits an {$affiliate->user->name} fΓΌr Referral {$referredUser->name}"); + } + + $this->info("{$qualified->count()} Referrals verarbeitet."); + } +} diff --git a/src/app/Console/Commands/ProcessMailQueue.php b/src/app/Console/Commands/ProcessMailQueue.php new file mode 100644 index 0000000..bc383b7 --- /dev/null +++ b/src/app/Console/Commands/ProcessMailQueue.php @@ -0,0 +1,196 @@ +where('status', 'pending') + ->orWhere(function ($q) { + $q->where('status', 'processing') + ->where('updated_at', '<', now()->subMinutes(2)); + }); + }) + ->where(function ($q) { + $q->whereNull('available_at') + ->orWhere('available_at', '<=', now()); + }) + ->orderBy('created_at') + ->limit(10) + ->get(); + + foreach ($jobs as $mail) { + + // πŸ”₯ ATOMIC LOCK + $updated = MailQueue::where('id', $mail->id) + ->where(function ($q) { + $q->where('status', 'pending') + ->orWhere(function ($q) { + $q->where('status', 'processing') + ->where('updated_at', '<', now()->subMinutes(2)); + }); + }) + ->limit(1) + ->update([ + 'status' => 'processing', + 'updated_at' => now(), + ]); + + if (!$updated) { + continue; + } + + try { + + app()->setLocale($mail->locale); + + $html = view('emails.' . $mail->template, $mail->meta)->render(); + + Mail::html($html, function ($message) use ($mail) { + $message->to($mail->to) + ->subject($mail->subject ?? 'Mail'); + }); + + $mail->update([ + 'sent_at' => now(), + 'status' => 'sent', + 'error' => null + ]); + + usleep(500000); + + } catch (\Throwable $e) { + + $tries = $mail->tries + 1; + $delay = 30 * $tries; + + $status = $tries >= $mail->max_tries ? 'failed' : 'pending'; + + $mail->update([ + 'tries' => $tries, + 'status' => $status, + 'error' => $e->getMessage(), + 'available_at' => $status === 'pending' + ? now()->addSeconds($delay) + : null, + ]); + } + } + + // πŸ”₯ dynamisches sleep + if ($jobs->isEmpty()) { + usleep(1000000); // idle + } else { + usleep(200000); // busy + } + } + } +} +// +//namespace App\Console\Commands; +// +//use App\Models\MailQueue; +//use Illuminate\Console\Attributes\Description; +//use Illuminate\Console\Attributes\Signature; +//use Illuminate\Console\Command; +//use Illuminate\Support\Facades\Mail; +// +//#[Signature('app:process-mail-queue')] +//#[Description('Command description')] +//class ProcessMailQueue extends Command +//{ +// /** +// * Execute the console command. +// */ +// public function handle() +// { +// while (true) { +// +// $jobs = MailQueue::where(function ($q) { +// $q->where('status', 'pending') +// ->orWhere(function ($q) { +// $q->where('status', 'processing') +// ->where('updated_at', '<', now()->subMinutes(2)); +// }); +// }) +// ->limit(10) +// ->orderBy('created_at') +// ->get(); +// +// $jobs->each(function ($mail) { +// +// // πŸ”₯ LOCK setzen +// $updated = MailQueue::where('id', $mail->id) +// ->where(function ($q) { +// $q->where('status', 'pending') +// ->orWhere(function ($q) { +// $q->where('status', 'processing') +// ->where('updated_at', '<', now()->subMinutes(2)); +// }); +// }) +// ->update([ +// 'status' => 'processing', +// 'updated_at' => now(), +// ]); +// +// // wenn schon verarbeitet β†’ skip +// if (!$updated) return; +// +// $mail->refresh(); +// +// try { +// +// app()->setLocale($mail->locale); +// +// $html = view('emails.' . $mail->template, $mail->meta)->render(); +// +// Mail::html($html, function ($message) use ($mail) { +// $message->to($mail->to) +// ->subject($mail->subject ?? 'Mail'); +// }); +// +// $mail->update([ +// 'sent_at' => now(), +// 'status' => 'sent', +// 'error' => null +// ]); +// +// } catch (\Throwable $e) { +// +// $tries = $mail->tries + 1; +// $delay = 30 * $tries; +// +// $status = $tries >= $mail->max_tries ? 'failed' : 'pending'; +// +// $mail->update([ +// 'tries' => $tries, +// 'status' => $status, +// 'error' => $e->getMessage(), +// 'available_at' => $status === 'pending' +// ? now()->addSeconds($delay) +// : null, +// ]); +// } +// +// }); +// +// if ($jobs->isEmpty()) { +// usleep(1000000); // 1s +// } else { +// usleep(200000); // 0.2s +// } +// } +// } +//} diff --git a/src/app/Console/Commands/ProcessMonthlyCredits.php b/src/app/Console/Commands/ProcessMonthlyCredits.php new file mode 100644 index 0000000..d7d9d74 --- /dev/null +++ b/src/app/Console/Commands/ProcessMonthlyCredits.php @@ -0,0 +1,59 @@ +subMonth(); + $processed = 0; + + User::whereHas('subscription', fn($q) => + $q->whereIn('status', ['active', 'gifted']) + )->each(function (User $user) use ($lastMonth, &$processed) { + + $planLimit = $user->subscription?->plan?->credit_limit ?? 0; + + // Unlimited β†’ nichts tun + if ($planLimit === 0) return; + + // Verbrauch letzten Monat + $usage = (int) $user->agentLogs() + ->whereYear('created_at', $lastMonth->year) + ->whereMonth('created_at', $lastMonth->month) + ->sum('credits'); + + // Unter Plan-Limit β†’ kein Bonus-Abzug + if ($usage <= $planLimit) return; + + $overUsage = $usage - $planLimit; + + // VerfΓΌgbares Guthaben prΓΌfen + $bonusLeft = max(0, (int) $user->creditTransactions()->sum('amount')); + + if ($bonusLeft <= 0) return; + + $deduct = min($overUsage, $bonusLeft); + + CreditTransaction::create([ + 'user_id' => $user->id, + 'amount' => -$deduct, + 'type' => 'usage', + 'description' => 'Bonus-Verbrauch ' . $lastMonth->translatedFormat('M Y') . ' (' . $overUsage . ' ΓΌber Plan-Limit)', + ]); + + $processed++; + $this->info("-{$deduct} Credits von {$user->name} ({$usage}/{$planLimit}, {$overUsage} ΓΌber Limit)"); + }); + + $this->info("{$processed} Bonus-Abbuchungen verarbeitet."); + } +} diff --git a/src/app/Console/Commands/RunAutomations.php b/src/app/Console/Commands/RunAutomations.php new file mode 100644 index 0000000..379c9bc --- /dev/null +++ b/src/app/Console/Commands/RunAutomations.php @@ -0,0 +1,103 @@ +runBirthdayReminders(); + } + + private function runBirthdayReminders(): void + { + $users = User::whereHas('automations', fn ($q) => $q + ->where('type', 'birthday_reminder') + ->where('active', true) + )->with(['automations' => fn ($q) => $q->where('type', 'birthday_reminder')])->get(); + + foreach ($users as $user) { + $auto = $user->automations->first(); + + $settings = []; + if (!empty($auto->settings)) { + $settings = is_array($auto->settings) + ? $auto->settings + : json_decode($auto->settings, true); + } elseif (!empty($auto->config)) { + $settings = is_array($auto->config) + ? $auto->config + : json_decode($auto->config, true); + } + + $daysBefore = (int) ($settings['days_before'] ?? 0); + $tz = $user->timezone ?? 'Europe/Vienna'; + $targetMonthDay = now($tz)->addDays($daysBefore)->format('m-d'); + + $contacts = \App\Models\Contact::where('user_id', $user->id) + ->whereNotNull('birthday') + ->get() + ->filter(fn ($c) => Carbon::parse($c->birthday)->format('m-d') === $targetMonthDay); + + foreach ($contacts as $contact) { + $hash = md5($user->id . $contact->id . now($tz)->format('Y-m-d') . 'birthday'); + + $alreadySent = DB::table('sent_reminders') + ->where('reminder_hash', $hash) + ->where('type', 'birthday') + ->whereDate('sent_at', now($tz)->toDateString()) + ->exists(); + + if ($alreadySent) continue; + + $pushText = [ + 'title' => $daysBefore === 0 + ? "Heute: {$contact->name} hat Geburtstag!" + : "Geburtstag: {$contact->name}", + 'body' => $daysBefore === 0 + ? 'Vergiss nicht zu gratulieren!' + : "In {$daysBefore} Tag(en)", + ]; + + try { + PushService::send( + $user, + $pushText['title'], + $pushText['body'], + ['type' => 'birthday_reminder', 'contact_id' => $contact->id] + ); + + DB::table('sent_reminders')->insert([ + 'id' => (string) Str::uuid(), + 'event_id' => null, + 'reminder_hash' => $hash, + 'type' => 'birthday', + 'sent_at' => now(), + ]); + + \Log::info('Birthday push sent', [ + 'user' => $user->name, + 'contact' => $contact->name, + ]); + } catch (\Throwable $e) { + \Log::error('Birthday push failed', [ + 'user' => $user->name, + 'contact' => $contact->name, + 'error' => $e->getMessage(), + ]); + } + } + } + } +} diff --git a/src/app/Console/Commands/ScheduleEventReminders.php b/src/app/Console/Commands/ScheduleEventReminders.php new file mode 100644 index 0000000..fdb069f --- /dev/null +++ b/src/app/Console/Commands/ScheduleEventReminders.php @@ -0,0 +1,246 @@ +copy()->startOfMinute(); + $minuteEnd = $now->copy()->endOfMinute(); + + $this->processTaskReminders($minuteStart, $minuteEnd); + + $events = Event::with('user') + ->where('starts_at', '>', $now) + ->where('starts_at', '<', $now->copy()->addDay()) + ->get(); + + Log::info('reminders:schedule run', [ + 'now_utc' => $now->toDateTimeString(), + 'window_utc' => $minuteEnd->toDateTimeString(), + 'events_found' => $events->count(), + ]); + + foreach ($events as $event) { + $user = $event->user; + if (!$user) continue; + + $tz = $user->timezone ?? 'Europe/Vienna'; + + // ══════════════════════════════════════ + // PFAD A β€” Event hat eigene Reminder + // ══════════════════════════════════════ + $eventReminders = $event->reminders ?? []; + if (is_string($eventReminders)) { + $eventReminders = json_decode($eventReminders, true) ?? []; + } + + if (!empty($eventReminders)) { + foreach ($eventReminders as $reminder) { + $sendAt = $this->calculateSendTime($event, $reminder, $tz); + + if (!$sendAt || !$sendAt->between($minuteStart, $minuteEnd)) { + continue; + } + + $hash = md5(json_encode($reminder)); + $sent = DB::table('sent_reminders') + ->where('event_id', $event->id) + ->where('reminder_hash', $hash) + ->exists(); + if ($sent) continue; + + $this->sendEventReminder($user, $event, $reminder, $tz); + + DB::table('sent_reminders')->insert([ + 'id' => (string) Str::uuid(), + 'event_id' => $event->id, + 'reminder_hash' => $hash, + 'sent_at' => now(), + ]); + } + continue; + } + + // ══════════════════════════════════════ + // PFAD B β€” Globale Automation + // ══════════════════════════════════════ + $globalAuto = $user->automations() + ->where('type', 'event_reminder') + ->where('active', true) + ->first(); + + if (!$globalAuto) continue; + + $minutesBefore = $globalAuto->cfg('minutes_before', 30); + $reminder = [ + 'type' => 'before', + 'minutes' => $minutesBefore, + ]; + + $sendAt = $this->calculateSendTime($event, $reminder, $tz); + + if (!$sendAt || !$sendAt->between($minuteStart, $minuteEnd)) { + continue; + } + + $hash = md5('global_' . $minutesBefore); + $sent = DB::table('sent_reminders') + ->where('event_id', $event->id) + ->where('reminder_hash', $hash) + ->exists(); + if ($sent) continue; + + $this->sendEventReminder($user, $event, $reminder, $tz); + + DB::table('sent_reminders')->insert([ + 'id' => (string) Str::uuid(), + 'event_id' => $event->id, + 'reminder_hash' => $hash, + 'sent_at' => now(), + ]); + } + } + + private function sendEventReminder(\App\Models\User $user, Event $event, array $reminder, string $tz): void + { + $start = $event->starts_at->setTimezone($tz); + $diffMin = (int) now($tz)->diffInMinutes($start, false); + + $body = match (true) { + $diffMin <= 0 => 'Jetzt!', + $diffMin < 60 => 'In ' . $diffMin . ' Minuten', + $diffMin < 1440 => 'In ' . intval($diffMin / 60) . ' Stunde(n)', + default => 'Morgen um ' . $start->format('H:i') . ' Uhr', + }; + + Log::info('Event push sending', [ + 'event' => $event->title, + 'diff_min' => $diffMin, + 'body' => $body, + ]); + + $hasPushToken = Device::where('user_id', $user->id) + ->where('active', true) + ->whereNotNull('push_token') + ->exists(); + + if ($hasPushToken) { + PushService::send( + $user, + 'Erinnerung: ' . $event->title, + $body, + ['type' => 'event_reminder', 'event_id' => $event->id] + ); + } + + Notification::create([ + 'user_id' => $user->id, + 'type' => 'event_reminder', + 'title' => $event->title, + 'message' => $body, + 'data' => ['event_id' => $event->id, 'reminder' => $reminder], + ]); + + Log::info('Event reminder sent', ['user' => $user->name, 'event' => $event->title]); + } + + private function processTaskReminders(Carbon $minuteStart, Carbon $minuteEnd): void + { + $tasks = Task::with('user') + ->whereNotNull('reminder_at') + ->where('reminder_at', '>=', $minuteStart) + ->where('reminder_at', '<=', $minuteEnd) + ->where('reminder_sent', false) + ->where('status', '!=', 'done') + ->get(); + + foreach ($tasks as $task) { + $user = $task->user; + if (!$user) continue; + + $tz = $user->timezone ?? 'Europe/Vienna'; + + $body = $task->due_at + ? 'FΓ€llig: ' . $task->due_at->setTimezone($tz)->format('H:i') . ' Uhr' + : 'Aufgabe erledigen'; + + $hasPushToken = Device::where('user_id', $user->id) + ->where('active', true) + ->whereNotNull('push_token') + ->exists(); + + if ($hasPushToken) { + PushService::send( + $user, + 'Erinnerung: ' . $task->title, + $body, + ['type' => 'task_reminder', 'task_id' => $task->id] + ); + } + + Notification::create([ + 'user_id' => $user->id, + 'type' => 'task_reminder', + 'title' => $task->title, + 'message' => $body, + 'data' => ['task_id' => $task->id], + ]); + + $task->update(['reminder_sent' => true]); + + Log::info('Task reminder sent', ['user' => $user->name, 'task' => $task->title]); + } + } + + private function calculateSendTime(Event $event, array $reminder, string $tz): ?Carbon + { + $startUtc = $event->starts_at; + + $sendAt = match ($reminder['type']) { + 'before' => $startUtc->copy()->subMinutes($reminder['minutes'] ?? 30), + + 'time_of_day' => Carbon::createFromFormat( + 'Y-m-d H:i', + $event->starts_at->setTimezone($tz)->format('Y-m-d') . ' ' . ($reminder['time'] ?? '08:00'), + $tz + )->utc(), + + 'day_before' => Carbon::createFromFormat( + 'Y-m-d H:i', + $event->starts_at->setTimezone($tz)->subDay()->format('Y-m-d') . ' ' . ($reminder['time'] ?? '18:00'), + $tz + )->utc(), + + default => null, + }; + + Log::info('calculateSendTime', [ + 'event' => $event->title, + 'reminder' => $reminder, + 'starts_utc' => $startUtc->toDateTimeString(), + 'send_at_utc' => $sendAt?->toDateTimeString(), + 'now_utc' => now()->toDateTimeString(), + 'diff_min' => $sendAt ? round(now()->diffInSeconds($sendAt, false) / 60, 2) : null, + ]); + + return $sendAt; + } +} diff --git a/src/app/Enums/PaymentStatus.php b/src/app/Enums/PaymentStatus.php new file mode 100644 index 0000000..984f7bc --- /dev/null +++ b/src/app/Enums/PaymentStatus.php @@ -0,0 +1,51 @@ + 'Ausstehend', + self::Paid => 'Bezahlt', + self::Failed => 'Fehlgeschlagen', + self::Refunded => 'Erstattet', + }; + } + + public function badgeBg(): string + { + return match($this) { + self::Pending => 'bg-amber-50', + self::Paid => 'bg-green-50', + self::Failed => 'bg-red-50', + self::Refunded => 'bg-blue-50', + }; + } + + public function badgeText(): string + { + return match($this) { + self::Pending => 'text-amber-700', + self::Paid => 'text-green-700', + self::Failed => 'text-red-700', + self::Refunded => 'text-blue-700', + }; + } + + public function icon(): string + { + return match($this) { + self::Pending => 'clock', + self::Paid => 'check', + self::Failed => 'x-mark', + self::Refunded => 'arrow-uturn-left', + }; + } +} diff --git a/src/app/Enums/SubscriptionStatus.php b/src/app/Enums/SubscriptionStatus.php new file mode 100644 index 0000000..dd48e51 --- /dev/null +++ b/src/app/Enums/SubscriptionStatus.php @@ -0,0 +1,49 @@ + 'Aktiv', + self::Canceled => 'GekΓΌndigt', + self::Expired => 'Abgelaufen', + self::PastDue => 'Zahlung offen', + self::Gifted => 'Geschenkt', + self::Superseded => 'Ersetzt', + }; + } + + public function badgeBg(): string + { + return match($this) { + self::Active => 'bg-green-50', + self::Canceled => 'bg-amber-50', + self::Expired => 'bg-red-50', + self::PastDue => 'bg-red-50', + self::Gifted => 'bg-purple-50', + self::Superseded => 'bg-gray-50', + }; + } + + public function badgeText(): string + { + return match($this) { + self::Active => 'text-green-700', + self::Canceled => 'text-amber-700', + self::Expired => 'text-red-700', + self::PastDue => 'text-red-700', + self::Gifted => 'text-purple-700', + self::Superseded => 'text-gray-500', + }; + } +} diff --git a/src/app/Enums/UserRole.php b/src/app/Enums/UserRole.php new file mode 100644 index 0000000..e081c00 --- /dev/null +++ b/src/app/Enums/UserRole.php @@ -0,0 +1,79 @@ + 'Nutzer', + self::BetaTester => 'Beta-Tester', + self::Affiliate => 'Affiliate', + self::Support => 'Support', + self::Developer => 'Developer', + self::Admin => 'Administrator', + self::SuperAdmin => 'Super Admin', + }; + } + + public function badgeBg(): string + { + return match($this) { + self::User => 'bg-gray-100', + self::BetaTester => 'bg-orange-50', + self::Affiliate => 'bg-fuchsia-50', + self::Support => 'bg-blue-50', + self::Developer => 'bg-green-50', + self::Admin => 'bg-purple-50', + self::SuperAdmin => 'bg-yellow-50', + }; + } + + public function badgeText(): string + { + return match($this) { + self::User => 'text-gray-500', + self::BetaTester => 'text-orange-700', + self::Affiliate => 'text-fuchsia-700', + self::Support => 'text-blue-700', + self::Developer => 'text-green-700', + self::Admin => 'text-purple-700', + self::SuperAdmin => 'text-yellow-700', + }; + } + + public function hierarchy(): int + { + return match($this) { + self::User => 0, + self::BetaTester => 1, + self::Affiliate => 1, + self::Support => 2, + self::Developer => 3, + self::Admin => 4, + self::SuperAdmin => 5, + }; + } + + public function icon(): string + { + return match($this) { + self::User => 'user', + self::BetaTester => 'beaker', + self::Affiliate => 'link', + self::Support => 'chat-bubble-left-right', + self::Developer => 'code-bracket', + self::Admin => 'cog-6-tooth', + self::SuperAdmin => 'shield-check', + }; + } +} diff --git a/src/app/Enums/UserStatus.php b/src/app/Enums/UserStatus.php new file mode 100644 index 0000000..383d057 --- /dev/null +++ b/src/app/Enums/UserStatus.php @@ -0,0 +1,10 @@ +userId}"), + ]; + } + + public function broadcastAs(): string + { + return 'calendar.updated'; + } +} diff --git a/src/app/Events/NotificationCreated.php b/src/app/Events/NotificationCreated.php new file mode 100644 index 0000000..02f26af --- /dev/null +++ b/src/app/Events/NotificationCreated.php @@ -0,0 +1,41 @@ +notification->user_id), + ]; + } + + public function broadcastAs(): string + { + return 'notification.created'; + } + + public function broadcastWith(): array + { + return [ + 'id' => $this->notification->id, + 'type' => $this->notification->type, + 'title' => $this->notification->title, + 'message' => $this->notification->message, + ]; + } +} diff --git a/src/app/Helpers/helpers.php b/src/app/Helpers/helpers.php new file mode 100644 index 0000000..343abb1 --- /dev/null +++ b/src/app/Helpers/helpers.php @@ -0,0 +1,42 @@ +getLocale(); + + $translations = cache()->rememberForever( + "translations:$locale", + function () use ($locale) { + return \App\Models\Translation::where('locale', $locale) + ->pluck('value', 'key') + ->toArray(); + } + ); + + $text = $translations[$key] ?? $key; + + foreach ($replace as $k => $v) { + $text = str_replace(":$k", $v, $text); + } + + return $text; +} + +//if (!function_exists('t')) { +// function t($key, $locale = null) +// { +// $locale = $locale ?? app()->getLocale(); +// +// $translations = cache()->rememberForever( +// "translations:$locale", +// function () use ($locale) { +// return \App\Models\Translation::where('locale', $locale) +// ->pluck('value', 'key') +// ->toArray(); +// } +// ); +// +// return $translations[$key] ?? $key; +// } +//} diff --git a/src/app/Http/Controllers/Api/AgentChatController.php b/src/app/Http/Controllers/Api/AgentChatController.php new file mode 100644 index 0000000..224d6ba --- /dev/null +++ b/src/app/Http/Controllers/Api/AgentChatController.php @@ -0,0 +1,320 @@ +validate([ + 'message' => 'required|string|max:2000', + 'conversation_history' => 'array', + 'conversation_history.*.role' => 'required_with:conversation_history|in:user,assistant', + 'conversation_history.*.content' => 'required_with:conversation_history|string', + 'with_audio' => 'boolean', + ]); + + $user = $request->user(); + $user->load('subscription.plan'); + + // Credit-Limit prΓΌfen + if ($user->effective_limit > 0 && $user->monthly_usage >= $user->effective_limit * 1.05) { + return response()->json([ + 'success' => false, + 'message' => 'Dein monatliches Credit-Limit ist erreicht.', + 'errors' => ['credits' => 'Limit ΓΌberschritten'], + ], 429); + } + + $aiConfig = $user->subscription?->plan?->ai_config ?? [ + 'model' => config('services.openai.model', 'gpt-4o-mini'), + 'temperature' => 0.5, + 'max_tokens' => 1500, + 'input_cost' => 0.00015, + 'output_cost' => 0.0006, + ]; + + // Konversation aufbauen + $history = $request->input('conversation_history', []); + $history[] = ['role' => 'user', 'content' => $request->message]; + + // User-Kontext erstellen (geteilter Service β†’ Web + API identisch) + $userContext = app(AgentContextService::class)->build($user); + + $startTime = microtime(true); + + // AI aufrufen + \Log::info('AgentChat: Calling AI', [ + 'user_id' => $user->id, + 'message' => mb_substr($request->message, 0, 100), + 'history_count' => count($history), + 'model' => $aiConfig['model'] ?? 'default', + ]); + + try { + $aiResult = AgentAIService::chat($history, $aiConfig, $userContext); + \Log::info('AgentChat: AI result', [ + 'type' => $aiResult['type'] ?? 'unknown', + 'has_message' => !empty($aiResult['data']['message'] ?? $aiResult['message'] ?? ''), + ]); + } catch (\Throwable $e) { + \Log::error('AgentChat: AI exception', [ + 'error' => $e->getMessage(), + 'class' => get_class($e), + ]); + report($e); + return response()->json([ + 'success' => true, + 'data' => [ + 'message' => 'Entschuldigung, ich konnte gerade nicht antworten. Bitte versuche es nochmal.', + 'action' => null, + 'type' => 'chat', + 'credits_used' => 0, + ], + ]); + } + + // _error-Flag: OpenAI API down oder Timeout β€” 0 Credits, Fehler loggen + if (!empty($aiResult['_error'])) { + $durationMs = (int) ((microtime(true) - $startTime) * 1000); + AgentLog::create([ + 'user_id' => $user->id, + 'type' => 'chat', + 'input' => mb_substr($request->message, 0, 100), + 'status' => 'error', + 'output' => null, + 'credits' => 0, + 'ai_response' => null, + 'model' => $aiConfig['model'] ?? null, + 'duration_ms' => $durationMs, + 'prompt_tokens' => 0, + 'completion_tokens' => 0, + 'total_tokens' => 0, + 'cost_usd' => 0, + ]); + return response()->json([ + 'success' => true, + 'data' => [ + 'message' => $aiResult['data']['message'] ?? 'Entschuldigung, bitte versuche es nochmal.', + 'action' => null, + 'type' => 'chat', + 'credits_used' => 0, + 'usage' => [ + 'credits_used' => $user->monthly_usage, + 'credits_limit' => $user->effective_limit, + 'usage_percent' => $user->usage_percent, + ], + ], + ]); + } + + // AgentAIService gibt direkt das parsed-Objekt zurΓΌck (kein 'parsed'-Wrapper) + // Keys: type, data, _usage, _multi (bei Multi-Actions) + $parsed = $aiResult; + $usage = $aiResult['_usage'] ?? []; + + $durationMs = (int) ((microtime(true) - $startTime) * 1000); + + $actionResult = null; + $assistantMessage = $parsed['data']['message'] + ?? $parsed['message'] + ?? $parsed['text'] + ?? ''; + + // Aktion ausfΓΌhren falls vorhanden + try { + if (isset($parsed['_multi'])) { + $actionService = new AgentActionService(); + $results = []; + foreach ($parsed['_multi'] as $action) { + $results[] = $actionService->handle($user, $action); + } + $actionResult = [ + 'status' => collect($results)->every(fn ($r) => $r['status'] === 'success') ? 'success' : 'partial', + 'results' => $results, + ]; + } elseif (isset($parsed['type']) && $parsed['type'] !== 'chat') { + $actionService = new AgentActionService(); + $actionResult = $actionService->handle($user, $parsed); + } + } catch (\Throwable $e) { + report($e); + $actionResult = ['status' => 'error', 'message' => 'Aktion fehlgeschlagen']; + } + + // Wenn Aktion ausgefΓΌhrt aber keine Message von der AI β†’ Action-Message verwenden + if (empty($assistantMessage) && $actionResult) { + $assistantMessage = $actionResult['message'] ?? 'Erledigt!'; + } + + // Letzter Schutz: JSON darf NIE vorgelesen werden + if (is_string($assistantMessage) && $assistantMessage !== '') { + $trim = ltrim($assistantMessage); + $isJsonLike = $trim !== '' && ($trim[0] === '{' || $trim[0] === '[') + || str_contains($assistantMessage, '"type":') + || str_contains($assistantMessage, '"data":'); + + if ($isJsonLike) { + \Log::warning('AgentChat: JSON leaked to assistant message β€” replaced', [ + 'preview' => mb_substr($assistantMessage, 0, 200), + ]); + $assistantMessage = $actionResult['message'] ?? 'Erledigt!'; + } + } + + // Credits berechnen β€” Flat-Rate-Logik + // - Aktionen: tokenbasiert (wie bisher) + // - Erster Chat einer Session (history leer): pauschal 5 Credits + // - Folge-Chat-Nachrichten: 0 Credits, kein Log + $type = $parsed['type'] ?? 'chat'; + $isAction = $type !== 'chat'; + $historyCount = count($request->input('conversation_history', [])); + $shouldLog = true; + + if ($isAction) { + $credits = (($actionResult['status'] ?? '') === 'error') + ? 0 + : $this->calculateCredits($usage, $aiConfig, $type); + } elseif ($historyCount === 0) { + $credits = 5; + } else { + $credits = 0; + $shouldLog = false; + } + + if ($shouldLog) { + // Input fΓΌr Log bestimmen (wie im Web) + $logInput = match ($type) { + 'chat' => 'Konversation', + 'event', 'event_update' => $parsed['data']['title'] ?? $request->message, + 'note', 'note_update' => mb_substr($parsed['data']['content'] ?? $request->message, 0, 100), + 'task', 'task_update' => $parsed['data']['title'] ?? $request->message, + 'contact' => $parsed['data']['name'] ?? $request->message, + 'email' => 'E-Mail an ' . ($parsed['data']['contact'] ?? '') . ': ' . mb_substr($parsed['data']['subject'] ?? '', 0, 50), + 'multi' => $request->message, + default => mb_substr($request->message, 0, 200), + }; + + $logStatus = ($actionResult['status'] ?? 'success') === 'error' ? 'failed' : ($actionResult['status'] ?? 'success'); + + AgentLog::create([ + 'user_id' => $user->id, + 'type' => $type, + 'input' => $logInput, + 'status' => $logStatus, + 'output' => $actionResult['meta'] ?? $actionResult ?? null, + 'credits' => $credits, + 'ai_response' => $parsed, + 'model' => $aiConfig['model'] ?? null, + 'duration_ms' => $durationMs, + 'prompt_tokens' => $usage['prompt_tokens'] ?? 0, + 'completion_tokens' => $usage['completion_tokens'] ?? 0, + 'total_tokens' => $usage['total_tokens'] ?? 0, + 'cost_usd' => $this->calculateCostUsd($usage, $aiConfig), + ]); + } + + // TTS falls gewΓΌnscht (Fehler darf Chat nicht blockieren) + $audio = null; + if ($request->boolean('with_audio') && $assistantMessage) { + try { + $audio = AgentAIService::textToSpeech($assistantMessage, $aiConfig); + } catch (\Throwable $e) { + report($e); + } + } + + // Usage neu berechnen + $user->refresh(); + + $responseData = [ + 'message' => $assistantMessage, + 'action' => $actionResult, + 'type' => $parsed['type'] ?? 'chat', + 'credits_used' => $credits, + 'usage' => [ + 'credits_used' => $user->monthly_usage, + 'credits_limit' => $user->effective_limit, + 'usage_percent' => $user->usage_percent, + ], + ]; + + if ($audio) { + $responseData['audio'] = $audio; + } + + return response()->json([ + 'success' => true, + 'data' => $responseData, + ]); + } + + public function synthesize(Request $request): JsonResponse + { + $request->validate([ + 'text' => 'required|string|max:500', + 'locale' => 'nullable|string', + ]); + + $user = $request->user(); + $aiConfig = $user->subscription?->plan?->ai_config ?? []; + if (is_string($aiConfig)) { + $aiConfig = json_decode($aiConfig, true); + } + + // Voice je nach Locale wΓ€hlen + $voice = str_starts_with($request->input('locale', 'de'), 'en') + ? 'shimmer' + : 'nova'; + + $overrideConfig = array_merge($aiConfig, ['tts_voice' => $voice]); + $audio = AgentAIService::textToSpeech($request->text, $overrideConfig); + + return response()->json([ + 'success' => true, + 'data' => ['audio' => $audio], + ]); + } + + + private function calculateCredits(array $usage, array $aiConfig, string $type): int + { + // Flat-Basis + Output-Tokens. Kontext (prompt_tokens) wird ignoriert, + // damit Credits nicht mit Kalendergrâße skalieren. Cap bei 100. + $completionTokens = (int) ($usage['completion_tokens'] ?? 0); + $credits = 20 + (int) ceil($completionTokens * 0.3); + + return min(100, max(1, $credits)); + } + + private function calculateCostUsd(array $usage, array $aiConfig): float + { + $inputCost = $aiConfig['input_cost'] ?? 0.00015; + $outputCost = $aiConfig['output_cost'] ?? 0.0006; + + return (($usage['prompt_tokens'] ?? 0) / 1000) * $inputCost + + (($usage['completion_tokens'] ?? 0) / 1000) * $outputCost; + } + + public function logs(Request $request): JsonResponse + { + $logs = $request->user() + ->agentLogs() + ->latest() + ->take(20) + ->get(['id', 'type', 'input', 'output', 'status', 'credits', 'duration_ms', 'created_at']); + + return response()->json([ + 'success' => true, + 'data' => $logs, + ]); + } +} diff --git a/src/app/Http/Controllers/Api/AuthController.php b/src/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000..3e33b9b --- /dev/null +++ b/src/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,87 @@ +validate([ + 'email' => 'required|email', + 'password' => 'required|string', + ]); + + $user = User::where('email', $request->email)->first(); + + if (! $user || ! Hash::check($request->password, $user->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Die Anmeldedaten sind ungΓΌltig.', + 'errors' => [], + ], 401); + } + + $token = Str::random(64); + $user->tokens()->create([ + 'token' => hash('sha256', $token), + 'name' => $request->header('User-Agent', 'API'), + ]); + + return response()->json([ + 'success' => true, + 'data' => [ + 'token' => $token, + 'user' => $user->only(['id', 'name', 'email', 'locale', 'timezone']), + ], + 'message' => 'Erfolgreich angemeldet.', + ]); + } + + public function logout(Request $request): JsonResponse + { + $bearer = $request->bearerToken(); + + if ($bearer) { + $request->user()->tokens() + ->where('token', hash('sha256', $bearer)) + ->delete(); + } + + return response()->json([ + 'success' => true, + 'data' => null, + 'message' => 'Erfolgreich abgemeldet.', + ]); + } + + public function me(Request $request): JsonResponse + { + $user = $request->user(); + $user->load('subscription.plan'); + + return response()->json([ + 'success' => true, + 'data' => [ + 'id' => $user->id, + 'locale' => $user->locale ?? 'de', + 'timezone' => $user->timezone ?? 'Europe/Vienna', + 'user' => $user->only(['id', 'name', 'email', 'locale', 'timezone', 'settings', 'role']), + 'plan' => $user->subscription?->plan?->only(['id', 'name', 'plan_key', 'credit_limit']), + 'usage' => [ + 'credits_used' => $user->monthly_usage, + 'credits_limit' => $user->effective_limit, + 'usage_percent' => $user->usage_percent, + 'bonus_credits' => $user->bonus_credits, + ], + ], + ]); + } +} diff --git a/src/app/Http/Controllers/Api/AutomationController.php b/src/app/Http/Controllers/Api/AutomationController.php new file mode 100644 index 0000000..df3c89e --- /dev/null +++ b/src/app/Http/Controllers/Api/AutomationController.php @@ -0,0 +1,125 @@ +user(); + $isPro = $user->isInternalUser() || $user->hasFeature('automations'); + $types = config('automations.types', []); + + \Log::info('Automation index', [ + 'user_id' => $user->id, + 'plan_key' => $user->subscription?->plan?->key ?? 'none', + 'is_pro' => $isPro, + 'free_types' => config('automations.free_types', []), + ]); + + $userAutomations = $user->automations()->get()->keyBy('type'); + + $locale = $user->locale ?? 'de'; + + $result = collect($types) + ->map(function (array $def, string $type) use ($userAutomations, $isPro, $locale) { + $userAuto = $userAutomations->get($type); + $isFree = (bool) ($def['is_free'] ?? false); + $name = t("automations.type.{$type}.name", [], $locale); + $desc = t("automations.type.{$type}.description", [], $locale); + return [ + 'type' => $type, + 'name' => ($name !== "automations.type.{$type}.name") ? $name : $def['name'], + 'description' => ($desc !== "automations.type.{$type}.description") ? $desc : $def['description'], + 'icon' => $def['icon_app'], + 'color' => $def['color_hex'], + 'is_free' => $isFree, + 'is_active' => $userAuto?->active ?? false, + 'is_configured' => $userAuto !== null, + 'available' => $isFree || $isPro, + 'channels' => $def['channels'] ?? ['push'], + 'settings' => $userAuto?->config ?? $def['defaults'], + 'settings_schema' => collect($def['defaults']) + ->except(['channel', 'weekdays_only']) + ->toArray(), + ]; + }) + ->values(); + + return response()->json([ + 'success' => true, + 'data' => $result, + ]); + } + + public function update(Request $request, string $type): JsonResponse + { + $types = config('automations.types'); + if (!isset($types[$type])) { + return response()->json(['success' => false, 'message' => 'Unbekannter Typ.'], 422); + } + + $user = $request->user(); + $def = $types[$type]; + $isPro = $user->isInternalUser() || $user->hasFeature('automations'); + + if (!$def['is_free'] && !$isPro) { + return response()->json(['success' => false, 'message' => 'Pro erforderlich.'], 403); + } + + $config = array_merge($def['defaults'], $request->input('settings', [])); + $is_active = $request->boolean('is_active', true); + + $user->automations()->updateOrCreate( + ['type' => $type], + ['name' => $def['name'], 'active' => $is_active, 'config' => $config] + ); + + return response()->json(['success' => true]); + } + + public function toggle(Request $request, string $type): JsonResponse + { + $types = config('automations.types'); + if (!isset($types[$type])) { + return response()->json(['success' => false, 'message' => 'Unbekannter Typ.'], 404); + } + + $user = $request->user(); + $def = $types[$type]; + $isPro = $user->isInternalUser() || $user->hasFeature('automations'); + + if (!(bool) ($def['is_free'] ?? false) && !$isPro) { + return response()->json(['success' => false, 'message' => 'Pro erforderlich.'], 403); + } + + $request->validate(['is_active' => 'boolean']); + + $automation = $user->automations()->firstOrCreate( + ['type' => $type], + ['name' => $def['name'], 'active' => false, 'config' => $def['defaults']] + ); + + $newActive = $request->has('is_active') + ? $request->boolean('is_active') + : !$automation->active; + + $automation->update(['active' => $newActive]); + + return response()->json([ + 'success' => true, + 'data' => ['type' => $type, 'is_active' => $automation->active], + ]); + } + + public function destroy(Request $request, string $type): JsonResponse + { + $request->user()->automations()->where('type', $type)->delete(); + return response()->json(['success' => true]); + } +} diff --git a/src/app/Http/Controllers/Api/ContactController.php b/src/app/Http/Controllers/Api/ContactController.php new file mode 100644 index 0000000..fda617a --- /dev/null +++ b/src/app/Http/Controllers/Api/ContactController.php @@ -0,0 +1,81 @@ +user()->contacts(); + + if ($request->has('search')) { + $query->search($request->search); + } + + $contacts = $query->orderBy('name')->get(); + + return response()->json([ + 'success' => true, + 'data' => $contacts, + ]); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'nullable|string|email|max:255', + 'phone' => 'nullable|string|max:50', + 'type' => 'in:privat,arbeit,kunde,sonstiges', + 'notes' => 'nullable|string|max:2000', + 'birthday' => 'nullable|date', + ]); + + $contact = $request->user()->contacts()->create($validated); + + return response()->json([ + 'success' => true, + 'data' => $contact, + 'message' => 'Kontakt erstellt.', + ], 201); + } + + public function update(Request $request, string $id): JsonResponse + { + $contact = $request->user()->contacts()->findOrFail($id); + + $validated = $request->validate([ + 'name' => 'sometimes|string|max:255', + 'email' => 'nullable|string|email|max:255', + 'phone' => 'nullable|string|max:50', + 'type' => 'in:privat,arbeit,kunde,sonstiges', + 'notes' => 'nullable|string|max:2000', + 'birthday' => 'nullable|date', + ]); + + $contact->update($validated); + + return response()->json([ + 'success' => true, + 'data' => $contact->fresh(), + 'message' => 'Kontakt aktualisiert.', + ]); + } + + public function destroy(Request $request, string $id): JsonResponse + { + $contact = $request->user()->contacts()->findOrFail($id); + $contact->delete(); + + return response()->json([ + 'success' => true, + 'data' => null, + 'message' => 'Kontakt gelΓΆscht.', + ]); + } +} diff --git a/src/app/Http/Controllers/Api/DashboardController.php b/src/app/Http/Controllers/Api/DashboardController.php new file mode 100644 index 0000000..6bd44f9 --- /dev/null +++ b/src/app/Http/Controllers/Api/DashboardController.php @@ -0,0 +1,63 @@ +user(); + $tz = $user->timezone; + $today = now($tz)->format('m-d'); + + $all = Contact::where('user_id', $user->id) + ->whereNotNull('birthday') + ->get(); + + $birthdaysToday = $all + ->filter(fn ($c) => Carbon::parse($c->birthday)->format('m-d') === $today) + ->values() + ->map(fn ($c) => [ + 'id' => $c->id, + 'name' => $c->name, + 'birthday' => $c->birthday, + 'age' => Carbon::parse($c->birthday)->year > 1900 + ? now()->year - Carbon::parse($c->birthday)->year + : null, + ]); + + $birthdaysSoon = $all + ->filter(function ($c) use ($tz) { + $days = now($tz)->diffInDays( + Carbon::parse($c->birthday)->setYear(now($tz)->year), + false + ); + return $days > 0 && $days <= 7; + }) + ->sortBy(fn ($c) => Carbon::parse($c->birthday)->format('m-d')) + ->values() + ->map(fn ($c) => [ + 'id' => $c->id, + 'name' => $c->name, + 'birthday' => $c->birthday, + 'days_until' => (int) now($tz)->diffInDays( + Carbon::parse($c->birthday)->setYear(now($tz)->year), + false + ), + ]); + + return response()->json([ + 'success' => true, + 'data' => [ + 'today' => $birthdaysToday, + 'soon' => $birthdaysSoon, + ], + ]); + } +} diff --git a/src/app/Http/Controllers/Api/DeviceController.php b/src/app/Http/Controllers/Api/DeviceController.php new file mode 100644 index 0000000..46ebcf5 --- /dev/null +++ b/src/app/Http/Controllers/Api/DeviceController.php @@ -0,0 +1,86 @@ +validate([ + 'device_id' => 'required|string', + 'push_token' => 'nullable|string', + 'platform' => 'required|in:ios,android', + 'device_name' => 'nullable|string|max:255', + ]); + + $device = Device::updateOrCreate( + [ + 'user_id' => auth()->id(), + 'device_id' => $request->device_id, + ], + [ + 'push_token' => $request->push_token, + 'platform' => $request->platform, + 'device_name' => $request->device_name, + 'active' => true, + 'last_seen' => now(), + ] + ); + + // Dasselbe Push-Token bei anderen Usern deaktivieren + if ($request->push_token) { + Device::where('push_token', $request->push_token) + ->where('user_id', '!=', auth()->id()) + ->update(['push_token' => null, 'active' => false]); + } + + return response()->json([ + 'success' => true, + 'data' => $device, + ], 201); + } + + public function updateToken(Request $request): JsonResponse + { + $request->validate([ + 'device_id' => 'required|string', + 'push_token' => 'required|string', + ]); + + Device::where('user_id', auth()->id()) + ->where('device_id', $request->device_id) + ->update([ + 'push_token' => $request->push_token, + 'active' => true, + 'last_seen' => now(), + ]); + + return response()->json(['success' => true]); + } + + public function deactivateToken(string $deviceId): JsonResponse + { + Device::where('user_id', auth()->id()) + ->where('device_id', $deviceId) + ->update(['push_token' => null, 'active' => false]); + + return response()->json(['success' => true]); + } + + public function destroy(Request $request, string $deviceId): JsonResponse + { + Device::where('user_id', auth()->id()) + ->where('device_id', $deviceId) + ->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'GerΓ€t entfernt.', + ]); + } +} diff --git a/src/app/Http/Controllers/Api/EventController.php b/src/app/Http/Controllers/Api/EventController.php new file mode 100644 index 0000000..32dd88e --- /dev/null +++ b/src/app/Http/Controllers/Api/EventController.php @@ -0,0 +1,116 @@ +validate([ + 'from' => 'required|date', + 'to' => 'required|date|after_or_equal:from', + ]); + + $events = $request->user()->events() + ->with('contacts') + ->where(function ($q) use ($request) { + $q->whereBetween('starts_at', [$request->from, $request->to]) + ->orWhereBetween('ends_at', [$request->from, $request->to]) + ->orWhere(function ($q2) use ($request) { + $q2->where('starts_at', '<=', $request->from) + ->where('ends_at', '>=', $request->to); + }); + }) + ->orderBy('starts_at') + ->get(); + + return response()->json([ + 'success' => true, + 'data' => $events, + ]); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'starts_at' => 'required_without:start_date|date', + 'ends_at' => 'nullable|date|after_or_equal:starts_at', + 'start_date' => 'required_without:starts_at|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'is_all_day' => 'boolean', + 'notes' => 'nullable|string', + 'color' => 'nullable|string|max:7', + 'reminders' => 'nullable|array', + 'reminders.*.type' => 'required|in:before,time_of_day,day_before', + 'reminders.*.minutes' => 'nullable|integer|min:1', + 'reminders.*.time' => 'nullable|date_format:H:i', + 'recurrence' => 'nullable|in:daily,weekly,monthly,yearly', + 'recurrence_end_date' => 'nullable|date', + 'attendee_ids' => 'nullable|array', + 'attendee_ids.*' => 'uuid|exists:contacts,id', + ]); + + $event = $request->user()->events()->create($validated); + $event->contacts()->sync($request->attendee_ids ?? []); + + return response()->json([ + 'success' => true, + 'data' => $event->load('contacts'), + 'message' => 'Termin erstellt.', + ], 201); + } + + public function update(Request $request, string $id): JsonResponse + { + $event = $request->user()->events()->findOrFail($id); + + $validated = $request->validate([ + 'title' => 'sometimes|string|max:255', + 'starts_at' => 'sometimes|date', + 'ends_at' => 'nullable|date|after_or_equal:starts_at', + 'start_date' => 'sometimes|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'is_all_day' => 'boolean', + 'notes' => 'nullable|string', + 'color' => 'nullable|string|max:7', + 'reminders' => 'nullable|array', + 'reminders.*.type' => 'required|in:before,time_of_day,day_before', + 'reminders.*.minutes' => 'nullable|integer|min:1', + 'reminders.*.time' => 'nullable|date_format:H:i', + 'recurrence' => 'nullable|in:daily,weekly,monthly,yearly', + 'recurrence_end_date' => 'nullable|date', + 'attendee_ids' => 'nullable|array', + 'attendee_ids.*' => 'uuid|exists:contacts,id', + ]); + + $event->update($validated); + + if ($request->has('attendee_ids')) { + $event->contacts()->sync($request->attendee_ids ?? []); + } + + return response()->json([ + 'success' => true, + 'data' => $event->fresh()->load('contacts'), + 'message' => 'Termin aktualisiert.', + ]); + } + + public function destroy(Request $request, string $id): JsonResponse + { + $event = $request->user()->events()->findOrFail($id); + $event->delete(); + + return response()->json([ + 'success' => true, + 'data' => null, + 'message' => 'Termin gelΓΆscht.', + ]); + } +} diff --git a/src/app/Http/Controllers/Api/NoteController.php b/src/app/Http/Controllers/Api/NoteController.php new file mode 100644 index 0000000..9d4ef20 --- /dev/null +++ b/src/app/Http/Controllers/Api/NoteController.php @@ -0,0 +1,79 @@ +user()->notes(); + + if ($request->has('search')) { + $query->search($request->search); + } + + $notes = $query->orderByDesc('pinned') + ->orderByDesc('updated_at') + ->get(); + + return response()->json([ + 'success' => true, + 'data' => $notes, + ]); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'content' => 'nullable|string', + 'color' => 'in:yellow,blue,green,pink,purple,gray', + 'pinned' => 'boolean', + ]); + + $note = $request->user()->notes()->create($validated); + + return response()->json([ + 'success' => true, + 'data' => $note, + 'message' => 'Notiz erstellt.', + ], 201); + } + + public function update(Request $request, string $id): JsonResponse + { + $note = $request->user()->notes()->findOrFail($id); + + $validated = $request->validate([ + 'title' => 'sometimes|string|max:255', + 'content' => 'nullable|string', + 'color' => 'in:yellow,blue,green,pink,purple,gray', + 'pinned' => 'boolean', + ]); + + $note->update($validated); + + return response()->json([ + 'success' => true, + 'data' => $note->fresh(), + 'message' => 'Notiz aktualisiert.', + ]); + } + + public function destroy(Request $request, string $id): JsonResponse + { + $note = $request->user()->notes()->findOrFail($id); + $note->delete(); + + return response()->json([ + 'success' => true, + 'data' => null, + 'message' => 'Notiz gelΓΆscht.', + ]); + } +} diff --git a/src/app/Http/Controllers/Api/SettingsController.php b/src/app/Http/Controllers/Api/SettingsController.php new file mode 100644 index 0000000..26b4a20 --- /dev/null +++ b/src/app/Http/Controllers/Api/SettingsController.php @@ -0,0 +1,207 @@ +user(); + + if ($request->isMethod('put')) { + $request->validate([ + 'channels' => 'array', + 'channels.in_app' => 'boolean', + 'channels.push' => 'boolean', + 'channels.email' => 'boolean', + ]); + + $channels = $request->input('channels', []); + + $updates = []; + if (isset($channels['in_app'])) $updates['notify_in_app'] = $channels['in_app']; + if (isset($channels['push'])) $updates['notify_push'] = $channels['push']; + if (isset($channels['email'])) $updates['notify_email'] = $channels['email']; + + if ($updates) { + $user->update($updates); + } + + // Devices aktivieren/deaktivieren je nach Push-Wunsch + if (isset($channels['push'])) { + $user->devices()->update(['active' => (bool) $channels['push']]); + } + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'channels' => [ + 'in_app' => (bool) ($user->notify_in_app ?? true), + 'push' => (bool) ($user->notify_push ?? false), + 'email' => (bool) ($user->notify_email ?? false), + ], + ], + ]); + } + + public function credits(Request $request): JsonResponse + { + $user = $request->user(); + + // Verbrauch aus AgentLogs (das ist die tatsΓ€chliche Nutzung). + // Label-PrioritΓ€t wie im Web: output.title > input > type. + $usage = $user->agentLogs() + ->orderByDesc('created_at') + ->take(30) + ->get() + ->map(fn ($log) => [ + 'type' => $log->type, + 'description' => $log->output['title'] ?? $log->input ?? $log->type, + 'amount' => -$log->credits, + 'duration_ms' => $log->duration_ms ?? 0, + 'created_at' => $log->created_at, + ]); + + // Bonus-Credits aus CreditTransactions + $bonuses = $user->creditTransactions() + ->orderByDesc('created_at') + ->take(10) + ->get() + ->map(fn ($tx) => [ + 'type' => $tx->type, + 'description' => $tx->description, + 'amount' => $tx->amount, + 'created_at' => $tx->created_at, + ]); + + // ZusammenfΓΌhren und nach Datum sortieren + $transactions = $usage->merge($bonuses) + ->sortByDesc('created_at') + ->values() + ->take(30); + + return response()->json([ + 'success' => true, + 'data' => [ + 'credits_used' => $user->effective_usage, + 'credit_limit' => $user->effective_limit, + 'bonus_credits' => $user->bonus_credits, + 'usage_percent' => $user->usage_percent, + 'transactions' => $transactions, + ], + ]); + } + + public function affiliate(Request $request): JsonResponse + { + $user = $request->user(); + $affiliate = $user->affiliate; + + if (! $affiliate) { + return response()->json([ + 'success' => true, + 'data' => null, + ]); + } + + $referrals = $affiliate->referrals() + ->with('referredUser') + ->latest() + ->get() + ->map(fn ($r) => [ + 'name' => $r->referredUser?->name ?? 'Unbekannt', + 'registered_at' => $r->registered_at, + 'qualifies_at' => $r->qualifies_at, + 'status' => $r->status, + 'credits_awarded' => $r->credits_awarded, + ]); + + return response()->json([ + 'success' => true, + 'data' => [ + 'code' => $affiliate->code, + 'referral_link' => $affiliate->referral_link, + 'referrals_count' => $affiliate->total_referrals, + 'qualified_count' => $affiliate->qualified_referrals, + 'credits_earned' => $affiliate->total_credits_earned, + 'status' => $affiliate->status, + 'referrals' => $referrals, + ], + ]); + } + + public function updateProfile(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => 'sometimes|string|max:255', + 'email' => 'sometimes|email|max:255', + 'locale' => 'sometimes|in:de,en', + 'timezone' => 'sometimes|string|max:100', + ]); + + $request->user()->update($validated); + + return response()->json([ + 'success' => true, + 'message' => 'Profil aktualisiert.', + ]); + } + + public function updatePassword(Request $request): JsonResponse + { + $request->validate([ + 'current_password' => 'required|string', + 'password' => ['required', 'confirmed', Password::min(6)], + ]); + + $user = $request->user(); + + if (! Hash::check($request->current_password, $user->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Aktuelles Passwort ist falsch.', + ], 422); + } + + $user->update(['password' => Hash::make($request->password)]); + + return response()->json([ + 'success' => true, + 'message' => 'Passwort geΓ€ndert.', + ]); + } + + public function deleteAccount(Request $request): JsonResponse + { + $user = $request->user(); + + // Alle Daten lΓΆschen + $user->events()->delete(); + $user->tasks()->delete(); + $user->notes()->delete(); + $user->contacts()->delete(); + $user->agentLogs()->delete(); + $user->creditTransactions()->delete(); + $user->tokens()->delete(); + + if ($user->affiliate) { + $user->affiliate->referrals()->delete(); + $user->affiliate->creditLogs()->delete(); + $user->affiliate->delete(); + } + + $user->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Konto gelΓΆscht.', + ]); + } +} diff --git a/src/app/Http/Controllers/Api/TaskController.php b/src/app/Http/Controllers/Api/TaskController.php new file mode 100644 index 0000000..05d1b32 --- /dev/null +++ b/src/app/Http/Controllers/Api/TaskController.php @@ -0,0 +1,139 @@ +user()->tasks(); + + if ($request->status === 'all') { + // Keine Filterung β€” alle Tasks zurΓΌckgeben + } elseif ($request->has('status')) { + $query->ofStatus($request->status); + } else { + $query->where('status', '!=', 'done'); + } + + if ($request->has('search')) { + $query->search($request->search); + } + + $tasks = $query->orderByRaw("FIELD(priority, 'high', 'medium', 'low')") + ->orderBy('due_at') + ->get(); + + return response()->json([ + 'success' => true, + 'data' => $tasks, + ]); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'priority' => 'in:low,medium,high', + 'due_at' => 'nullable|date', + 'reminder_at' => 'nullable|date', + ]); + + if ($request->filled('reminder_at')) { + $limitError = $this->checkReminderLimit($request->user()); + if ($limitError) return $limitError; + } + + $validated['status'] = 'pending'; + $validated['reminder_sent'] = false; + + $task = $request->user()->tasks()->create($validated); + + return response()->json([ + 'success' => true, + 'data' => $task, + 'message' => 'Aufgabe erstellt.', + ], 201); + } + + public function update(Request $request, string $id): JsonResponse + { + $task = $request->user()->tasks()->findOrFail($id); + + $validated = $request->validate([ + 'title' => 'sometimes|string|max:255', + 'description' => 'nullable|string', + 'status' => 'in:pending,in_progress,done', + 'priority' => 'in:low,medium,high', + 'due_at' => 'nullable|date', + 'reminder_at' => 'nullable|date', + ]); + + if ($request->filled('reminder_at') && $request->reminder_at !== optional($task->reminder_at)->toDateTimeString()) { + $limitError = $this->checkReminderLimit($request->user(), $task->id); + if ($limitError) return $limitError; + } + + if (isset($validated['status']) && $validated['status'] === 'done' && ! $task->isDone()) { + $validated['completed_at'] = now(); + } + + if (array_key_exists('reminder_at', $validated)) { + $validated['reminder_sent'] = false; + } + + $task->update($validated); + + return response()->json([ + 'success' => true, + 'data' => $task->fresh(), + 'message' => 'Aufgabe aktualisiert.', + ]); + } + + private function checkReminderLimit(\App\Models\User $user, ?string $excludeTaskId = null): ?\Illuminate\Http\JsonResponse + { + $isPro = $user->subscription?->plan?->plan_key !== 'free'; + if ($isPro) return null; + + $query = Task::where('user_id', $user->id) + ->whereNotNull('reminder_at') + ->where('reminder_at', '>', now()) + ->where('reminder_sent', false) + ->where('status', '!=', 'done'); + + if ($excludeTaskId) { + $query->where('id', '!=', $excludeTaskId); + } + + $count = $query->count(); + + if ($count >= 3) { + return response()->json([ + 'success' => false, + 'message' => 'limit_reached', + 'data' => ['limit' => 3, 'current' => $count], + ], 422); + } + + return null; + } + + public function destroy(Request $request, string $id): JsonResponse + { + $task = $request->user()->tasks()->findOrFail($id); + $task->delete(); + + return response()->json([ + 'success' => true, + 'data' => null, + 'message' => 'Aufgabe gelΓΆscht.', + ]); + } +} diff --git a/src/app/Http/Controllers/Api/TranslationController.php b/src/app/Http/Controllers/Api/TranslationController.php new file mode 100644 index 0000000..8c5bad3 --- /dev/null +++ b/src/app/Http/Controllers/Api/TranslationController.php @@ -0,0 +1,21 @@ +pluck('value', 'key'); + + return response()->json([ + 'success' => true, + 'data' => $translations, + ]); + } +} diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/src/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ + $state]); + + $query = http_build_query([ + 'client_id' => config('services.google_calendar.client_id'), + 'redirect_uri' => config('services.google_calendar.redirect'), + 'response_type' => 'code', + 'scope' => 'https://www.googleapis.com/auth/calendar openid email profile', + 'access_type' => 'offline', + 'prompt' => 'consent', + 'state' => $state, + ]); + + return redirect(self::AUTH_URL.'?'.$query); + } + + public function callback(Request $request) + { + // CSRF-State prΓΌfen + if ($request->state !== session('google_oauth_state')) { + return redirect()->route('integrations.index') + ->with('integration_error', 'UngΓΌltiger State-Parameter.'); + } + + if ($request->has('error')) { + return redirect()->route('integrations.index') + ->with('integration_error', 'Zugriff verweigert: '.$request->error); + } + + // Code gegen Token tauschen + $response = Http::asForm()->post(self::TOKEN_URL, [ + 'code' => $request->code, + 'client_id' => config('services.google_calendar.client_id'), + 'client_secret' => config('services.google_calendar.client_secret'), + 'redirect_uri' => config('services.google_calendar.redirect'), + 'grant_type' => 'authorization_code', + ]); + + if ($response->failed()) { + return redirect()->route('integrations.index') + ->with('integration_error', 'Token-Austausch fehlgeschlagen.'); + } + + $tokens = $response->json(); + + // E-Mail des Google-Accounts holen + $userInfo = Http::withToken($tokens['access_token']) + ->get(self::USERINFO_URL) + ->json(); + + // PrimΓ€ren Kalender-Namen holen + $calendarInfo = Http::withToken($tokens['access_token']) + ->get(self::CALENDAR_URL) + ->json(); + + CalendarIntegration::updateOrCreate( + ['user_id' => auth()->id(), 'provider' => 'google'], + [ + 'provider_email' => $userInfo['email'] ?? null, + 'calendar_id' => 'primary', + 'calendar_name' => $calendarInfo['summary'] ?? 'PrimΓ€rer Kalender', + 'access_token' => $tokens['access_token'], + 'refresh_token' => $tokens['refresh_token'] ?? null, + 'token_expires_at' => now()->addSeconds($tokens['expires_in'] ?? 3600), + ] + ); + + return redirect()->route('integrations.index') + ->with('integration_connected', 'google'); + } +} diff --git a/src/app/Http/Controllers/Integration/OutlookCalendarController.php b/src/app/Http/Controllers/Integration/OutlookCalendarController.php new file mode 100644 index 0000000..d1b39f9 --- /dev/null +++ b/src/app/Http/Controllers/Integration/OutlookCalendarController.php @@ -0,0 +1,96 @@ + $state]); + + $query = http_build_query([ + 'client_id' => config('services.microsoft.client_id'), + 'redirect_uri' => config('services.microsoft.redirect'), + 'response_type' => 'code', + 'scope' => 'openid email profile Calendars.ReadWrite offline_access', + 'state' => $state, + ]); + + return redirect($this->authUrl().'?'.$query); + } + + public function callback(Request $request) + { + if ($request->state !== session('outlook_oauth_state')) { + return redirect()->route('integrations.index') + ->with('integration_error', 'UngΓΌltiger State-Parameter.'); + } + + if ($request->has('error')) { + return redirect()->route('integrations.index') + ->with('integration_error', 'Zugriff verweigert: '.$request->error_description); + } + + $response = Http::asForm()->post($this->tokenUrl(), [ + 'code' => $request->code, + 'client_id' => config('services.microsoft.client_id'), + 'client_secret' => config('services.microsoft.client_secret'), + 'redirect_uri' => config('services.microsoft.redirect'), + 'grant_type' => 'authorization_code', + ]); + + if ($response->failed()) { + return redirect()->route('integrations.index') + ->with('integration_error', 'Token-Austausch fehlgeschlagen.'); + } + + $tokens = $response->json(); + + $me = Http::withToken($tokens['access_token']) + ->get(self::ME_URL) + ->json(); + + $calendars = Http::withToken($tokens['access_token']) + ->get(self::CALENDAR_URL) + ->json(); + + $primaryCalendar = $calendars['value'][0] ?? null; + + CalendarIntegration::updateOrCreate( + ['user_id' => auth()->id(), 'provider' => 'outlook'], + [ + 'provider_email' => $me['mail'] ?? $me['userPrincipalName'] ?? null, + 'calendar_id' => $primaryCalendar['id'] ?? null, + 'calendar_name' => $primaryCalendar['name'] ?? 'PrimΓ€rer Kalender', + 'access_token' => $tokens['access_token'], + 'refresh_token' => $tokens['refresh_token'] ?? null, + 'token_expires_at' => now()->addSeconds($tokens['expires_in'] ?? 3600), + ] + ); + + return redirect()->route('integrations.index') + ->with('integration_connected', 'outlook'); + } +} diff --git a/src/app/Http/Controllers/Stripe/WebhookController.php b/src/app/Http/Controllers/Stripe/WebhookController.php new file mode 100644 index 0000000..a777ba8 --- /dev/null +++ b/src/app/Http/Controllers/Stripe/WebhookController.php @@ -0,0 +1,29 @@ +getContent(); + $signature = $request->header('Stripe-Signature', ''); + + try { + $stripe->handleWebhook($payload, $signature); + } catch (SignatureVerificationException $e) { + return response('Invalid signature', 400); + } catch (\Throwable $e) { + report($e); + return response('Webhook error: ' . $e->getMessage(), 500); + } + + return response('OK', 200); + } +} diff --git a/src/app/Http/Controllers/VerifyController.php b/src/app/Http/Controllers/VerifyController.php new file mode 100644 index 0000000..259e367 --- /dev/null +++ b/src/app/Http/Controllers/VerifyController.php @@ -0,0 +1,78 @@ +user; + + // πŸ”₯ schon verifiziert + if ($user->email_verified_at) { + return redirect()->route('dashboard.index'); + } + + // πŸ”₯ abgelaufen + if ($verification->isExpired()) { + return redirect()->route('verify.notice', [ + 'user' => $user->id + ])->with('error', t('auth.verify.expired')); + } + + // πŸ”₯ schon benutzt + if ($verification->isVerified()) { + return redirect()->route('dashboard.index'); + } + + // πŸ”₯ USER UPDATEN + if ($verification->type === 'email') { + $user->update([ + 'email_verified_at' => now(), + ]); + } + + Auth::login($user); + + Verification::where('user_id', $user->id) + ->where('type', $verification->type) + ->delete(); + + return redirect()->route('dashboard.index') + ->with('success', t('auth.verify.success')); + } +} + +// +//namespace App\Http\Controllers; +// +//use App\Models\User; +//use Illuminate\Http\Request; +//use Illuminate\Support\Facades\Auth; +// +//class VerifyController extends Controller +//{ +// public function verify(User $user) +// { +// if ($user->email_verified_at) { +// return redirect()->route('dashboard.index'); +// } +// +// $user->update([ +// 'email_verified_at' => now(), +// 'email_verification_code' => null, +// 'email_verification_expires_at' => null, +// 'verification_resends' => 0, +// 'verification_resends_reset_at' => null, +// ]); +// +// Auth::login($user); +// +// return redirect()->route('dashboard.index') +// ->with('success', t('auth.verify.success')); +// } +//} diff --git a/src/app/Http/Controllers/Webhooks/GoogleCalendarWebhookController.php b/src/app/Http/Controllers/Webhooks/GoogleCalendarWebhookController.php new file mode 100644 index 0000000..1608243 --- /dev/null +++ b/src/app/Http/Controllers/Webhooks/GoogleCalendarWebhookController.php @@ -0,0 +1,53 @@ +header('X-Goog-Channel-Id'); + $resourceState = $request->header('X-Goog-Resource-State'); + + // Erster Ping nach Watch-Registrierung β€” nur bestΓ€tigen + if ($resourceState === 'sync') { + Log::info('Google Calendar Webhook: Sync-Ping empfangen', ['channel_id' => $channelId]); + return response('', 200); + } + + if (!$channelId || $resourceState !== 'exists') { + return response('', 200); + } + + $integration = CalendarIntegration::where('watch_channel_id', $channelId)->first(); + + if (!$integration) { + Log::warning('Google Calendar Webhook: Unbekannter Channel', [ + 'channel_id' => $channelId, + ]); + return response('', 200); + } + + Log::info('Google Calendar Webhook: Γ„nderung empfangen β†’ Sync starten', [ + 'user_id' => $integration->user_id, + 'channel_id' => $channelId, + ]); + + // Pull-Sync als Job dispatchen (nicht blockierend) + SyncFromGoogleCalendarJob::dispatch($integration->id); + + return response('', 200); + } +} diff --git a/src/app/Http/Middleware/CheckAppVersion.php b/src/app/Http/Middleware/CheckAppVersion.php new file mode 100644 index 0000000..ae9e93d --- /dev/null +++ b/src/app/Http/Middleware/CheckAppVersion.php @@ -0,0 +1,22 @@ + true]); + session(['app_version' => $currentVersion]); + } + + return $next($request); + } +} diff --git a/src/app/Http/Middleware/EnsureAuthenticated.php b/src/app/Http/Middleware/EnsureAuthenticated.php new file mode 100644 index 0000000..7cf3afe --- /dev/null +++ b/src/app/Http/Middleware/EnsureAuthenticated.php @@ -0,0 +1,46 @@ +bearerToken()) { + + $user = \App\Models\User::whereHas('tokens', function ($q) use ($token) { + $q->where('token', hash('sha256', $token)); + })->first(); + + if ($user) { + Auth::login($user); + $user->tokens()->where('token', hash('sha256', $token))->update(['last_used_at' => now()]); + return $next($request); + } + } + + if ($request->expectsJson()) { + return response()->json([ + 'message' => 'Unauthenticated', + ], 401); + } + + return redirect()->route('login'); + } +} diff --git a/src/app/Http/Middleware/EnsureIsAdmin.php b/src/app/Http/Middleware/EnsureIsAdmin.php new file mode 100644 index 0000000..bb61858 --- /dev/null +++ b/src/app/Http/Middleware/EnsureIsAdmin.php @@ -0,0 +1,19 @@ +check() || !auth()->user()->isAdmin()) { + abort(403, 'Kein Zugriff.'); + } + + return $next($request); + } +} diff --git a/src/app/Http/Middleware/EnsureUserIsVerified.php b/src/app/Http/Middleware/EnsureUserIsVerified.php new file mode 100644 index 0000000..b64d4cd --- /dev/null +++ b/src/app/Http/Middleware/EnsureUserIsVerified.php @@ -0,0 +1,25 @@ +route('login'); + } + + return $next($request); + } +} diff --git a/src/app/Http/Middleware/ForceJsonResponse.php b/src/app/Http/Middleware/ForceJsonResponse.php new file mode 100644 index 0000000..753249e --- /dev/null +++ b/src/app/Http/Middleware/ForceJsonResponse.php @@ -0,0 +1,17 @@ +headers->set('Accept', 'application/json'); + + return $next($request); + } +} diff --git a/src/app/Http/Middleware/RedirectIfAuthenticatedCustom.php b/src/app/Http/Middleware/RedirectIfAuthenticatedCustom.php new file mode 100644 index 0000000..60172fe --- /dev/null +++ b/src/app/Http/Middleware/RedirectIfAuthenticatedCustom.php @@ -0,0 +1,24 @@ +route('dashboard.index'); + } + return $next($request); + } +} diff --git a/src/app/Http/Middleware/RoleMiddleware.php b/src/app/Http/Middleware/RoleMiddleware.php new file mode 100644 index 0000000..2441f67 --- /dev/null +++ b/src/app/Http/Middleware/RoleMiddleware.php @@ -0,0 +1,38 @@ +check()) { + return redirect()->route('login'); + } + + $user = auth()->user(); + + // Super Admin hat immer Zugang + if ($user->isSuperAdmin()) { + return $next($request); + } + + // PrΓΌfe ob User mindestens eine der erlaubten Rollen hat (mit Hierarchie) + if (!empty($roles)) { + $hasAccess = collect($roles)->some( + fn(string $role) => $user->hasAtLeastRole($role) + ); + + if (!$hasAccess) { + abort(403, 'Keine Berechtigung.'); + } + } + + return $next($request); + } +} diff --git a/src/app/Http/Middleware/SetLocale.php b/src/app/Http/Middleware/SetLocale.php new file mode 100644 index 0000000..66c7c83 --- /dev/null +++ b/src/app/Http/Middleware/SetLocale.php @@ -0,0 +1,44 @@ +check()) { + $userLocale = auth()->user()->locale; + + if (in_array($userLocale, $available)) { + $locale = $userLocale; + // Session-Backup damit AJAX-Requests auch ohne frischen DB-Fetch korrekt sind + session(['locale' => $locale]); + } elseif (session()->has('locale') && in_array(session('locale'), $available)) { + $locale = session('locale'); + } + } elseif (session()->has('locale') && in_array(session('locale'), $available)) { + $locale = session('locale'); + } else { + $browser = $request->getPreferredLanguage($available); + if ($browser) { + $locale = $browser; + } + } + + app()->setLocale($locale); + + return $next($request); + } +} diff --git a/src/app/Http/Middleware/TrackAffiliateCode.php b/src/app/Http/Middleware/TrackAffiliateCode.php new file mode 100644 index 0000000..ef54d97 --- /dev/null +++ b/src/app/Http/Middleware/TrackAffiliateCode.php @@ -0,0 +1,18 @@ +has('ref')) { + session(['affiliate_ref_code' => $request->get('ref')]); + } + + return $next($request); + } +} diff --git a/src/app/Http/Middleware/TrustProxies.php b/src/app/Http/Middleware/TrustProxies.php new file mode 100644 index 0000000..e12acfd --- /dev/null +++ b/src/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,29 @@ +event->user; + + if (!$user) { + return; + } + + \Log::info('SendEventReminder: starting', [ + 'event' => $this->event->title, + 'user' => $user->name, + 'reminder' => $this->reminder, + ]); + + $settings = $user->notification_settings ?? []; + $emailEnabled = $settings['email_enabled'] ?? false; + $isPro = $user->subscription?->isActive() ?? false; + $isOwnReminder = !empty($this->event->reminders ?? []); + + $minutes = $this->reminder['minutes'] ?? 30; + $tz = $user->timezone ?? 'Europe/Vienna'; + $start = $this->event->starts_at->setTimezone($tz); + + $body = match (true) { + $minutes >= 1440 => 'Morgen um ' . $start->format('H:i') . ' Uhr', + $minutes >= 60 => 'In ' . intval($minutes / 60) . ' Stunde(n)', + default => 'In ' . $minutes . ' Minuten', + }; + + // PUSH β€” wenn Token vorhanden (kein Pro/push_enabled Gate) + $hasPushToken = $user->devices() + ->where('active', true) + ->whereNotNull('push_token') + ->exists(); + + \Log::info('SendEventReminder: user check', [ + 'plan_key' => $user->subscription?->plan?->plan_key, + 'hasPushToken' => $hasPushToken, + 'isOwnReminder'=> $isOwnReminder, + ]); + + if ($hasPushToken) { + PushService::send( + $user, + 'Erinnerung: ' . $this->event->title, + $body, + ['type' => 'event_reminder', 'event_id' => $this->event->id] + ); + + \Log::info('Event push sent', [ + 'event' => $this->event->title, + 'user' => $user->name, + ]); + } else { + \Log::warning('SendEventReminder: push skipped', ['reason' => 'no_active_token']); + } + + // EMAIL + if ($emailEnabled && ($isOwnReminder || $isPro)) { + try { + Mail::mailer(MailerService::forAutomation('event_reminder')) + ->to($user->email) + ->send(new \App\Mail\EventReminderMail($this->event, $this->reminder)); + } catch (\Throwable $e) { + \Log::warning('EventReminder: Email failed', ['error' => $e->getMessage()]); + } + } + + // IN-APP β€” immer + $this->sendInApp($user); + } + + private function sendInApp(User $user): void + { + $tz = $user->timezone ?? 'Europe/Vienna'; + $start = $this->event->starts_at->setTimezone($tz); + $minutes = $this->reminder['minutes'] ?? 30; + + $message = match (true) { + $minutes >= 1440 => 'Morgen um ' . $start->format('H:i') . ' Uhr', + $minutes >= 60 => 'In ' . ($minutes / 60) . ' Stunde(n)', + default => 'In ' . $minutes . ' Minuten', + }; + + Notification::create([ + 'user_id' => $user->id, + 'type' => 'event_reminder', + 'title' => $this->event->title, + 'message' => $message, + 'data' => ['event_id' => $this->event->id, 'reminder' => $this->reminder], + ]); + } + + public function buildMessage(): string + { + $tz = $this->event->user->timezone ?? 'Europe/Vienna'; + $start = $this->event->starts_at->setTimezone($tz); + + return match ($this->reminder['type']) { + 'before' => match (true) { + ($this->reminder['minutes'] ?? 0) >= 1440 => + 'Morgen: ' . $this->event->title . ' um ' . $start->format('H:i') . ' Uhr', + ($this->reminder['minutes'] ?? 0) >= 60 => + $this->event->title . ' in ' . (($this->reminder['minutes'] ?? 60) / 60) . ' Stunde(n)', + default => + $this->event->title . ' in ' . ($this->reminder['minutes'] ?? 30) . ' Minuten', + }, + 'time_of_day', 'day_before' => + 'Heute: ' . $this->event->title . ' um ' . $start->format('H:i') . ' Uhr', + default => $this->event->title, + }; + } +} diff --git a/src/app/Jobs/SyncFromGoogleCalendarJob.php b/src/app/Jobs/SyncFromGoogleCalendarJob.php new file mode 100644 index 0000000..845c267 --- /dev/null +++ b/src/app/Jobs/SyncFromGoogleCalendarJob.php @@ -0,0 +1,52 @@ +integrationId); + + if (!$integration) { + Log::warning('SyncFromGoogleCalendarJob: Integration nicht gefunden', [ + 'id' => $this->integrationId, + ]); + return; + } + + if (!in_array($integration->sync_mode, ['read', 'both'])) { + return; // Kein Pull fΓΌr diesen Modus + } + + Log::info('Google Calendar: Starte Pull-Sync', [ + 'user_id' => $integration->user_id, + 'sync_mode' => $integration->sync_mode, + 'has_token' => (bool) $integration->sync_token, + ]); + + $service->pullEvents($integration); + } +} diff --git a/src/app/Listeners/GiveBonusCredits.php b/src/app/Listeners/GiveBonusCredits.php new file mode 100644 index 0000000..eb2a842 --- /dev/null +++ b/src/app/Listeners/GiveBonusCredits.php @@ -0,0 +1,68 @@ +user; + $role = $user->role?->value ?? 'user'; + + // Keine Onboarding-Credits fΓΌr interne Rollen + $skipCredits = in_array($role, [ + 'admin', + 'super_admin', + 'developer', + 'support', + ]); + + if (!$skipCredits) { + CreditTransaction::onboarding($user, 300); + } + + // Kein Affiliate-Account fΓΌr interne Rollen + $skipAffiliate = in_array($role, [ + 'admin', + 'super_admin', + 'developer', + 'support', + ]); + + if (!$skipAffiliate) { + Affiliate::create([ + 'user_id' => $user->id, + 'code' => Affiliate::generateCode($user), + 'status' => 'active', + ]); + } + + // Referral tracken β€” nur fΓΌr nicht-interne User + if (!$skipCredits) { + $refCode = session()->pull('affiliate_ref_code'); + if ($refCode) { + $affiliate = Affiliate::where('code', $refCode) + ->where('status', 'active') + ->first(); + + if ($affiliate && $affiliate->user_id !== $user->id) { + AffiliateReferral::create([ + 'affiliate_id' => $affiliate->id, + 'referred_user_id' => $user->id, + 'affiliate_code' => $refCode, + 'status' => 'pending', + 'registered_at' => now(), + 'qualifies_at' => now()->addMonths(3), + ]); + + $affiliate->increment('total_referrals'); + } + } + } + } +} diff --git a/src/app/Livewire/Activities/Index.php b/src/app/Livewire/Activities/Index.php new file mode 100644 index 0000000..01eb6a6 --- /dev/null +++ b/src/app/Livewire/Activities/Index.php @@ -0,0 +1,73 @@ +resetPage(); } + public function updatedFilterType(): void { $this->resetPage(); } + + // ── LΓΆschen ─────────────────────────────────────────────────────────── + + public function delete(string $id): void + { + Activity::where('user_id', auth()->id())->where('id', $id)->delete(); + } + + public function clearAll(): void + { + Activity::where('user_id', auth()->id())->delete(); + $this->resetPage(); + } + + // ── Render ──────────────────────────────────────────────────────────── + + public function render() + { + $userId = auth()->id(); + $tz = auth()->user()->timezone ?? 'UTC'; + + $query = Activity::forUser($userId)->latest(); + + if ($this->search !== '') { + $query->search($this->search); + } + + if ($this->filterType !== '') { + $query->ofType($this->filterType); + } + + $activities = $query->paginate(20); + + // Datum-Gruppen fΓΌr die Timeline (nach User-TZ) + $grouped = $activities->getCollection() + ->groupBy(fn($a) => $a->created_at->setTimezone($tz)->toDateString()); + + // Statistiken (nur fΓΌr ungefilterte Ansicht sinnvoll) + $stats = Activity::forUser($userId) + ->selectRaw(" + count(*) as total, + sum(case when DATE(created_at) = CURRENT_DATE then 1 else 0 end) as today, + sum(case when type like 'event%' then 1 else 0 end) as events, + sum(case when type like 'automation%' then 1 else 0 end) as automations + ") + ->first(); + + return view('livewire.activities.index', [ + 'activities' => $activities, + 'grouped' => $grouped, + 'stats' => $stats, + 'tz' => $tz, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Admin/Affiliates/Index.php b/src/app/Livewire/Admin/Affiliates/Index.php new file mode 100644 index 0000000..60c91bc --- /dev/null +++ b/src/app/Livewire/Admin/Affiliates/Index.php @@ -0,0 +1,69 @@ +resetPage(); + } + + public function toggleStatus(string $affiliateId) + { + $affiliate = Affiliate::findOrFail($affiliateId); + $affiliate->update([ + 'status' => $affiliate->status === 'active' ? 'paused' : 'active', + ]); + + $this->dispatch('notify', type: 'success', message: 'Affiliate-Status aktualisiert.'); + } + + public function banAffiliate(string $affiliateId) + { + Affiliate::findOrFail($affiliateId)->update(['status' => 'banned']); + $this->dispatch('notify', type: 'success', message: 'Affiliate gesperrt.'); + } + + public function render() + { + $affiliates = Affiliate::with('user') + ->whereHas('user', fn($q) => + $q->whereNotIn('role', ['admin', 'super_admin', 'developer', 'support']) + ) + ->withCount([ + 'referrals', + 'referrals as pending_count' => fn($q) => $q->where('status', 'pending'), + 'referrals as paid_count' => fn($q) => $q->where('status', 'paid'), + ]) + ->when($this->search, fn($q) => + $q->whereHas('user', fn($uq) => + $uq->where('name', 'like', '%' . $this->search . '%') + ->orWhere('email', 'like', '%' . $this->search . '%') + )->orWhere('code', 'like', '%' . $this->search . '%') + ) + ->orderByDesc('total_credits_earned') + ->paginate(25); + + $stats = [ + 'total_affiliates' => Affiliate::count(), + 'active_affiliates' => Affiliate::where('status', 'active')->count(), + 'total_referrals' => AffiliateReferral::count(), + 'pending_referrals' => AffiliateReferral::where('status', 'pending')->count(), + 'credits_paid_out' => AffiliateCreditLog::sum('credits'), + ]; + + return view('livewire.admin.affiliates.index', compact('affiliates', 'stats')) + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Admin/Dashboard.php b/src/app/Livewire/Admin/Dashboard.php new file mode 100644 index 0000000..5cf4294 --- /dev/null +++ b/src/app/Livewire/Admin/Dashboard.php @@ -0,0 +1,24 @@ + User::count(), + 'activeUsers' => User::where('status', UserStatus::Active)->count(), + 'newThisMonth' => User::where('created_at', '>=', now()->startOfMonth())->count(), + 'proUsers' => User::whereHas('subscription', fn($q) => $q->where('status', 'active'))->count(), + 'totalCreditsUsed' => AgentLog::where('created_at', '>=', now()->startOfMonth())->sum('credits'), + 'totalCost' => AgentLog::where('created_at', '>=', now()->startOfMonth())->sum('cost_usd'), + 'recentLogs' => AgentLog::with('user')->latest()->limit(10)->get(), + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Admin/Features/Modals/Form.php b/src/app/Livewire/Admin/Features/Modals/Form.php new file mode 100644 index 0000000..6fee0bb --- /dev/null +++ b/src/app/Livewire/Admin/Features/Modals/Form.php @@ -0,0 +1,95 @@ + [ + 'heroicon-o-check', + 'heroicon-o-x-mark', + 'heroicon-o-star', + 'heroicon-o-bolt', + ], + 'User' => [ + 'heroicon-o-user', + 'heroicon-o-users', + ], + 'Zeit' => [ + 'heroicon-o-calendar', + 'heroicon-o-clock', + ], + 'System' => [ + 'heroicon-o-cog', + 'heroicon-o-wrench', + ], + ]; + + public $group_id = null; + public $active = true; + + public $groups = []; + + public function mount($id = null) + { + $this->groups = FeatureGroup::orderBy('sort')->get(); + + if ($id) { + $feature = Feature::findOrFail($id); + + $this->featureId = $feature->id; + $this->label = $feature->label; + $this->key = $feature->key; + $this->icon = $feature->icon; + $this->group_id = $feature->group_id; + $this->active = $feature->active; + } + } + + public function updatedLabel($value) + { + if (!$this->featureId) { + $this->key = Str::slug($value); + } + } + + public function save() + { + $this->validate([ + 'label' => 'required|string|max:255', + 'key' => 'required|string|max:255', + 'group_id' => 'nullable|exists:feature_groups,id', + ]); + + $feature = Feature::updateOrCreate( + ['id' => $this->featureId], + [ + 'label' => $this->label, + 'key' => $this->key, + 'icon' => $this->icon, + 'feature_group_id' => $this->group_id, + 'active' => $this->active, + ] + ); + + // πŸ‘‰ wichtig fΓΌr Plan Modal reload + $this->dispatch('featureCreated', featureId: $feature->id); + $this->dispatch('closeModal'); + } + + public function render() + { + return view('livewire.admin.features.modals.form'); + } +} diff --git a/src/app/Livewire/Admin/Plans/Index.php b/src/app/Livewire/Admin/Plans/Index.php new file mode 100644 index 0000000..cd98a58 --- /dev/null +++ b/src/app/Livewire/Admin/Plans/Index.php @@ -0,0 +1,35 @@ + 'loadPlans']; + + public function mount() + { + $this->loadPlans(); + } + + public function loadPlans() + { + $this->plans = Plan::orderBy('price')->get(); + } + + public function delete($id) + { + Plan::find($id)?->delete(); + $this->loadPlans(); + } + + public function render() + { + return view('livewire.admin.plans.index') + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Admin/Plans/Modals/Form.php b/src/app/Livewire/Admin/Plans/Modals/Form.php new file mode 100644 index 0000000..8305b44 --- /dev/null +++ b/src/app/Livewire/Admin/Plans/Modals/Form.php @@ -0,0 +1,191 @@ + 'reloadFeatures', + ]; + + public function mount($id = null) + { + // πŸ”₯ GROUPS + FEATURES LADEN + $this->groupedFeatures = FeatureGroup::with([ + 'features' => fn ($q) => $q->where('active', true)->orderBy('sort') + ]) + ->orderBy('sort') + ->get(); + + if ($id) { + $plan = Plan::with('features')->findOrFail($id); + + $this->planId = $plan->id; + $this->ai_config = $plan->ai_config ?? []; + $this->name = $plan->name; + $this->price = $plan->price; + + $this->credit_limit = $plan->credit_limit; + $this->device_limit = $plan->device_limit; + + $this->yearly_discount_months = $plan->yearly_discount_months ?? 0; + + // πŸ”₯ WICHTIG: KEIN ? mehr + $this->selectedFeatures = $plan->features + ? $plan->features->pluck('id')->toArray() + : []; + + $this->active = $plan->active; + $this->is_featured = $plan->is_featured; + } + } + + public function reloadFeatures($featureId = null) + { + $this->groupedFeatures = \App\Models\FeatureGroup::with([ + 'features' => fn ($q) => $q->where('active', true)->orderBy('sort') + ]) + ->orderBy('sort') + ->get(); + + // πŸ‘‰ optional direkt auswΓ€hlen + if ($featureId) { + $this->selectedFeatures[] = $featureId; + } + } + + public function toggleFeature($id) + { + if (in_array($id, $this->selectedFeatures)) { + $this->selectedFeatures = array_values( + array_diff($this->selectedFeatures, [$id]) + ); + } else { + $this->selectedFeatures[] = $id; + } + } + + public function nextStep() + { + $this->validateStep1(); + $this->step = 2; + } + + public function prevStep() + { + $this->step = 1; + } + + public function updated($field) + { + if (in_array($field, ['price', 'credit_limit', 'device_limit', 'yearly_discount_months'])) { + if ($this->$field < 0) { + $this->$field = 0; + } + } + + if ($field === 'yearly_discount_months' && $this->yearly_discount_months > 12) { + $this->yearly_discount_months = 12; + } + + if ($field === 'price' && $this->price == 0) { + $this->yearly_discount_months = 0; + } + } + + protected function validateStep1() + { + $this->validate([ + 'name' => 'required|string|max:255', + 'price' => 'required|integer|min:0', + 'credit_limit' => 'required|integer|min:0', + 'device_limit' => 'required|integer|min:0', + 'yearly_discount_months' => 'nullable|integer|min:0|max:12', + ]); + } + + + public function getAiModelDefaults(): array + { + $model = $this->ai_config['model'] ?? null; + + if (!$model) return []; + + return config('ai_models.' . $model) ?? []; + } + + public function save() + { + // πŸ”₯ AI CONFIG sauber aufbauen (Defaults + Overrides) + $modelKey = $this->ai_config['model'] ?? null; + + $finalAiConfig = []; + + if ($modelKey) { + $defaults = config('ai_models.' . $modelKey) ?? []; + + // πŸ‘‰ nur relevante Felder ΓΌbernehmen + $finalAiConfig = [ + 'model_key' => $modelKey, + 'model' => $defaults['model'] ?? null, + 'max_tokens' => $this->ai_config['max_tokens'] ?? $defaults['max_tokens'] ?? null, + 'temperature' => $this->ai_config['temperature'] ?? $defaults['temperature'] ?? null, + ]; + } + + $this->validateStep1(); + + $plan = Plan::updateOrCreate( + ['id' => $this->planId], + [ + 'name' => $this->name, + 'plan_key' => Str::slug($this->name), + 'price' => $this->price, + 'credit_limit' => $this->credit_limit, + 'device_limit' => $this->device_limit, + 'yearly_discount_months' => $this->price > 0 ? $this->yearly_discount_months : 0, + 'active' => $this->active, + 'is_featured' => $this->is_featured, + 'ai_config' => $finalAiConfig, + ] + ); + + // πŸ”₯ PIVOT SYNC + $plan->features()->sync($this->selectedFeatures); + + $this->dispatch('closeModal'); + $this->dispatch('planUpdated'); + } + + public function render() + { + return view('livewire.admin.plans.modals.form'); + } +} diff --git a/src/app/Livewire/Admin/Translations/Index.php b/src/app/Livewire/Admin/Translations/Index.php new file mode 100644 index 0000000..487df67 --- /dev/null +++ b/src/app/Livewire/Admin/Translations/Index.php @@ -0,0 +1,141 @@ + 'loadTranslations']; + + public function mount() + { + $this->loadTranslations(); + } + + public function updatedLocale() + { + $this->loadTranslations(); + } + + public function updatedSearch() + { + $this->loadTranslations(); + } + + public function updatedOnlyMissing() + { + $this->loadTranslations(); + } + + public function loadTranslations() + { + $query = Translation::query()->select('key')->distinct(); + + // πŸ” SEARCH + if ($this->search) { + $query->where('key', 'like', '%' . $this->search . '%'); + } + + $keys = $query->pluck('key'); + + $groups = []; + + foreach ($keys as $key) { + + $values = Translation::where('key', $key) + ->pluck('value', 'locale') + ->toArray(); + + // πŸ”₯ FILTER: NUR FEHLENDE + if ($this->onlyMissing) { + if (count($values) === count(config('app.locales'))) { + continue; + } + } + + $group = explode('.', $key)[0]; + + $groups[$group][] = [ + 'key' => $key, + 'values' => $values, + ]; + } + + $this->grouped = $groups; + } + + + public function updateField($index, $field, $value) + { + $item = $this->translations[$index]; + + if (!$item['id']) { + $translation = Translation::create([ + 'key' => $item['key'], + 'locale' => $this->locale, + 'value' => $value, + ]); + + $this->translations[$index]['id'] = $translation->id; + $this->translations[$index]['missing'] = false; + } else { + Translation::where('id', $item['id'])->update([ + $field => $value + ]); + } + + $this->translations[$index][$field] = $value; + } + + + public function updateValue($index, $value) + { + $item = $this->translations[$index]; + + Translation::where('id', $item['id'])->update([ + 'value' => $value + ]); + + // lokal updaten + $this->translations[$index]['value'] = $value; + } + + public function addNew() + { + $translation = Translation::create([ + 'key' => 'new.key.' . uniqid(), + 'locale' => $this->locale, + 'value' => '', + ]); + + $this->translations[] = [ + 'id' => $translation->id, + 'key' => $translation->key, + 'value' => '', + 'missing' => false, // πŸ”₯ wichtig + ]; + } + + + public function deleteKey($key) + { + Translation::where('key', $key)->delete(); + + $this->loadTranslations(); + } + + public function render() + { + return view('livewire.admin.translations.index') + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Admin/Translations/Modals/DeleteModal.php b/src/app/Livewire/Admin/Translations/Modals/DeleteModal.php new file mode 100644 index 0000000..39677d8 --- /dev/null +++ b/src/app/Livewire/Admin/Translations/Modals/DeleteModal.php @@ -0,0 +1,42 @@ +key = $key; + $this->count = Translation::where('key', $key)->count(); + } + + public function getCanDeleteProperty() + { + return $this->confirmKey === $this->key; + } + + public function delete() + { + if (!$this->canDelete) { + return; + } + + Translation::where('key', $this->key)->delete(); + + $this->dispatch('translations:updated'); + + $this->closeModal(); + } + + public function render() + { + return view('livewire.admin.translations.modals.delete-modal'); + } +} diff --git a/src/app/Livewire/Admin/Translations/Modals/EditModal.php b/src/app/Livewire/Admin/Translations/Modals/EditModal.php new file mode 100644 index 0000000..05a2e4b --- /dev/null +++ b/src/app/Livewire/Admin/Translations/Modals/EditModal.php @@ -0,0 +1,54 @@ +key = $key; + + foreach (config('app.locales') as $code => $label) { + + $this->values[$code] = Translation::where('key', $key) + ->where('locale', $code) + ->value('value') ?? ''; + } + } + + public function save() + { + if (!$this->key) { + $this->addError('key', 'Key erforderlich'); + return; + } + + foreach ($this->values as $locale => $value) { + + Translation::updateOrCreate( + [ + 'key' => $this->key, + 'locale' => $locale, + ], + [ + 'value' => $value + ] + ); + } + + $this->dispatch('translations:updated'); + + $this->closeModal(); + } + + public function render() + { + return view('livewire.admin.translations.modals.edit-modal'); + } +} diff --git a/src/app/Livewire/Admin/Users/Calendar.php b/src/app/Livewire/Admin/Users/Calendar.php new file mode 100644 index 0000000..faac6e8 --- /dev/null +++ b/src/app/Livewire/Admin/Users/Calendar.php @@ -0,0 +1,46 @@ +user = $user; + } + + public function openEvent(string $eventId) + { + $this->selectedEventId = $eventId; + $this->sidebarOpen = true; + } + + public function closeSidebar() + { + $this->sidebarOpen = false; + $this->selectedEventId = null; + } + + public function render() + { + $events = $this->user->events() + ->orderBy('starts_at', 'desc') + ->get(); + + $selectedEvent = $this->selectedEventId + ? Event::find($this->selectedEventId) + : null; + + return view('livewire.admin.users.calendar', compact('events', 'selectedEvent')) + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Admin/Users/Detail.php b/src/app/Livewire/Admin/Users/Detail.php new file mode 100644 index 0000000..3766a74 --- /dev/null +++ b/src/app/Livewire/Admin/Users/Detail.php @@ -0,0 +1,314 @@ +user = $user; + $this->newRole = $user->role->value; + } + + public function giveCredits() + { + $this->validate([ + 'creditAmount' => 'required|integer|min:1|max:10000', + 'creditReason' => 'required|string|max:255', + ]); + + CreditTransaction::adminGift( + $this->user, + $this->creditAmount, + $this->creditReason, + auth()->user() + ); + + $this->dispatch('notify', type: 'success', message: $this->creditAmount . ' Credits an ' . $this->user->name . ' vergeben.'); + + $this->creditAmount = 0; + $this->creditReason = ''; + $this->user->refresh(); + } + + public function giftAccess() + { + $this->validate([ + 'giftPlanId' => 'required|exists:plans,id', + 'giftDuration' => 'required|in:1_month,3_months,6_months,1_year,forever', + 'giftReason' => 'nullable|string|max:255', + ]); + + $plan = Plan::findOrFail($this->giftPlanId); + + $endsAt = match($this->giftDuration) { + '1_month' => now()->addMonth(), + '3_months' => now()->addMonths(3), + '6_months' => now()->addMonths(6), + '1_year' => now()->addYear(), + 'forever' => null, + }; + + $durationLabel = match($this->giftDuration) { + '1_month' => '1 Monat', + '3_months' => '3 Monate', + '6_months' => '6 Monate', + '1_year' => '1 Jahr', + 'forever' => 'Unbegrenzt', + }; + + // Bestehende aktive Subscriptions deaktivieren + $this->user->subscriptions() + ->whereIn('status', [SubscriptionStatus::Active->value, SubscriptionStatus::Gifted->value]) + ->update(['status' => SubscriptionStatus::Superseded->value]); + + // Neue Geschenk-Subscription erstellen + $this->user->subscriptions()->create([ + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'plan_slug' => $plan->plan_key ?? str($plan->name)->slug(), + 'price' => 0, + 'interval' => 'gifted', + 'status' => SubscriptionStatus::Gifted->value, + 'starts_at' => now(), + 'ends_at' => $endsAt, + 'gifted_by' => auth()->id(), + 'gifted_at' => now(), + 'gift_reason' => $this->giftReason ?: null, + ]); + + // Activity Log + Activity::log( + $this->user->id, + 'gift_access', + "Admin hat {$plan->name} ({$durationLabel}) geschenkt", + auth()->user()->name, + meta: [ + 'plan' => $plan->name, + 'duration' => $durationLabel, + 'ends_at' => $endsAt?->toDateString() ?? 'unbegrenzt', + 'gifted_by' => auth()->user()->name, + 'reason' => $this->giftReason, + ] + ); + + // Mail senden + try { + Mail::to($this->user->email)->send( + new \App\Mail\GiftAccessMail($this->user, $plan, $durationLabel, $endsAt) + ); + } catch (\Throwable $e) { + report($e); + } + + $this->dispatch('notify', type: 'success', message: "{$plan->name} fΓΌr {$durationLabel} an {$this->user->name} vergeben."); + + $this->giftPlanId = ''; + $this->giftDuration = '1_month'; + $this->giftReason = ''; + $this->user->refresh(); + } + + public function revokeAccess() + { + $this->user->subscriptions() + ->whereIn('status', [SubscriptionStatus::Active->value, SubscriptionStatus::Gifted->value]) + ->update([ + 'status' => SubscriptionStatus::Canceled->value, + 'ends_at' => now(), + ]); + + // Auf Free-Plan zurΓΌcksetzen + $freePlan = Plan::freePlan(); + + if ($freePlan) { + $this->user->subscriptions()->create([ + 'plan_id' => $freePlan->id, + 'plan_name' => $freePlan->name, + 'plan_slug' => $freePlan->plan_key ?? str($freePlan->name)->slug(), + 'price' => 0, + 'interval' => 'monthly', + 'status' => SubscriptionStatus::Active->value, + 'starts_at' => now(), + 'ends_at' => null, + ]); + } + + Activity::log( + $this->user->id, + 'revoke_access', + 'Admin hat Zugang entzogen', + auth()->user()->name, + ); + + $this->dispatch('notify', type: 'success', message: "Zugang von {$this->user->name} wurde entzogen."); + $this->user->refresh(); + } + + public function deleteLog(string $logId) + { + $log = AgentLog::findOrFail($logId); + abort_if($log->user_id !== $this->user->id, 403); + + $credits = $log->credits; + + CreditTransaction::refund( + $this->user, + $credits, + 'Log-LΓΆschung durch Admin', + $log->id, + auth()->user() + ); + + $log->delete(); + + $this->user->refresh(); + $this->dispatch('notify', type: 'success', message: $credits . ' Credits wurden zurΓΌckerstattet.'); + } + + public function updateStatus(string $status) + { + $this->user->update(['status' => $status]); + $this->user->refresh(); + + $this->dispatch('notify', type: 'success', message: 'Status aktualisiert.'); + } + + public function updateRole() + { + Gate::authorize('change-roles'); + + $this->validate([ + 'newRole' => 'required|in:' . implode(',', array_column(UserRole::cases(), 'value')), + ]); + + $newRole = UserRole::from($this->newRole); + + // Super Admin kann sich nicht selbst degradieren + if ($this->user->isSuperAdmin() && auth()->id() === $this->user->id) { + $this->dispatch('notify', type: 'error', message: 'Du kannst deine eigene Super-Admin Rolle nicht Γ€ndern.'); + return; + } + + // Niemand kann einen anderen Super Admin degradieren + if ($this->user->isSuperAdmin() && $newRole !== UserRole::SuperAdmin) { + $this->dispatch('notify', type: 'error', message: 'Super Admin Rollen kΓΆnnen nicht geΓ€ndert werden.'); + return; + } + + $oldRole = $this->user->role; + $this->user->update(['role' => $newRole->value]); + + // Auto-Plan-Zuweisung passend zur neuen Rolle + $this->assignPlanForRole($newRole); + + Activity::log( + $this->user->id, + 'role_change', + "Rolle geΓ€ndert: {$oldRole->label()} β†’ {$newRole->label()}", + auth()->user()->name, + meta: [ + 'old_role' => $oldRole->value, + 'new_role' => $newRole->value, + 'changed_by' => auth()->user()->name, + ] + ); + + $this->user->refresh(); + $this->dispatch('notify', type: 'success', message: "Rolle auf {$newRole->label()} geΓ€ndert."); + } + + private function assignPlanForRole(UserRole $role): void + { + $planKey = match($role) { + UserRole::SuperAdmin => 'internal', + UserRole::Admin => 'internal', + UserRole::Developer => 'developer', + UserRole::Support => 'support', + UserRole::Affiliate => 'affiliate', + UserRole::BetaTester => 'beta_tester', + UserRole::User => null, + }; + + if ($planKey === null) { + $plan = Plan::freePlan(); + } else { + $plan = Plan::where('plan_key', $planKey)->first(); + } + + if (!$plan) return; + + // Bestehende aktive Subscription deaktivieren + $this->user->subscriptions() + ->whereIn('status', [SubscriptionStatus::Active->value, SubscriptionStatus::Gifted->value]) + ->update(['status' => SubscriptionStatus::Superseded->value]); + + // Neuen Plan zuweisen + $this->user->subscriptions()->create([ + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'plan_slug' => $plan->plan_key ?? str($plan->name)->slug(), + 'price' => 0, + 'interval' => $planKey ? 'internal' : 'monthly', + 'status' => SubscriptionStatus::Active->value, + 'starts_at' => now(), + 'ends_at' => null, + ]); + } + + public function render() + { + $stats = [ + 'events' => $this->user->events()->count(), + 'notes' => $this->user->notes()->count(), + 'tasks' => $this->user->tasks()->count(), + 'contacts' => $this->user->contacts()->count(), + 'creditsUsed' => $this->user->agentLogs()->where('created_at', '>=', now()->startOfMonth())->sum('credits'), + 'totalCost' => $this->user->agentLogs()->sum('cost_usd'), + 'lastActive' => $this->user->agentLogs()->latest()->value('created_at'), + ]; + + $logs = $this->user->agentLogs()->latest()->paginate(10); + + $activeSub = $this->user->subscriptions() + ->whereIn('status', [SubscriptionStatus::Active->value, SubscriptionStatus::Gifted->value]) + ->with(['plan', 'gifter']) + ->latest() + ->first(); + + $plans = Plan::public()->where('price', '>', 0)->orderBy('price')->get(); + + $creditTx = $this->user->creditTransactions()->with('creator')->latest()->limit(15)->get(); + + return view('livewire.admin.users.detail', compact('stats', 'logs', 'activeSub', 'plans', 'creditTx')) + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Admin/Users/Index.php b/src/app/Livewire/Admin/Users/Index.php new file mode 100644 index 0000000..6cff9f7 --- /dev/null +++ b/src/app/Livewire/Admin/Users/Index.php @@ -0,0 +1,75 @@ + ['except' => ''], + 'tab' => ['except' => 'all'], + ]; + + public function updatingSearch() + { + $this->resetPage(); + } + + public function setTab(string $tab) + { + $this->tab = $tab; + $this->resetPage(); + } + + public function updateStatus(string $userId, string $status) + { + $user = User::findOrFail($userId); + $user->update(['status' => $status]); + + $this->dispatch('notify', type: 'success', message: 'Status aktualisiert.'); + } + + public function render() + { + $query = User::withCount(['events', 'agentLogs']) + ->with('subscription.plan') + ->when($this->search, fn($q) => + $q->where('name', 'like', '%' . $this->search . '%') + ->orWhere('email', 'like', '%' . $this->search . '%') + ); + + // Tab-Counts (vor Status-Filter) + $baseQuery = User::when($this->search, fn($q) => + $q->where('name', 'like', '%' . $this->search . '%') + ->orWhere('email', 'like', '%' . $this->search . '%') + ); + + $counts = [ + 'all' => (clone $baseQuery)->count(), + 'active' => (clone $baseQuery)->where('status', 'active')->count(), + 'suspended' => (clone $baseQuery)->where('status', 'suspended')->count(), + 'blocked' => (clone $baseQuery)->where('status', 'blocked')->count(), + 'staff' => (clone $baseQuery)->whereNotIn('role', ['user'])->count(), + ]; + + // Tab-Filter anwenden + if ($this->tab === 'staff') { + $query->whereNotIn('role', ['user']); + } elseif ($this->tab !== 'all') { + $query->where('status', $this->tab); + } + + $users = $query->latest()->paginate(25); + + return view('livewire.admin.users.index', compact('users', 'counts')) + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Admin/Versions.php b/src/app/Livewire/Admin/Versions.php new file mode 100644 index 0000000..6a6bad3 --- /dev/null +++ b/src/app/Livewire/Admin/Versions.php @@ -0,0 +1,83 @@ +get(); + } + + public function save(): void + { + $this->validate([ + 'version' => 'required', + 'name' => 'required', + 'status' => 'required|in:draft,beta,live,rollback', + 'platform' => 'required|in:all,web,app', + ]); + + $data = [ + 'version' => $this->version, + 'name' => $this->name, + 'changelog' => $this->changelog, + 'status' => $this->status, + 'platform' => $this->platform, + 'show_popup' => $this->show_popup, + 'released_at' => $this->status === 'live' ? now() : null, + ]; + + if ($this->editingId) { + AppVersion::find($this->editingId)->update($data); + } else { + AppVersion::create($data); + } + + $this->reset(['version', 'name', 'changelog', 'show_popup', 'editingId']); + $this->status = 'draft'; + $this->platform = 'all'; + } + + public function setLive(string $id): void + { + AppVersion::find($id)->update([ + 'status' => 'live', + 'released_at' => now(), + ]); + } + + public function edit(string $id): void + { + $v = AppVersion::find($id); + $this->editingId = $id; + $this->version = $v->version; + $this->name = $v->name; + $this->changelog = $v->changelog ?? ''; + $this->status = $v->status; + $this->platform = $v->platform; + $this->show_popup = $v->show_popup; + } + + public function delete(string $id): void + { + AppVersion::find($id)->delete(); + } + + public function render() + { + return view('livewire.admin.versions') + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Agent/History.php b/src/app/Livewire/Agent/History.php new file mode 100644 index 0000000..3350768 --- /dev/null +++ b/src/app/Livewire/Agent/History.php @@ -0,0 +1,36 @@ + 'reload']; + + public function mount(): void + { + $this->load(); + } + + public function reload(): void + { + $this->load(); + } + + private function load(): void + { + $this->history = AgentLog::where('user_id', auth()->id()) + ->latest() + ->limit(8) + ->get(); + } + + public function render() + { + return view('livewire.agent.history'); + } +} diff --git a/src/app/Livewire/Agent/Index.php b/src/app/Livewire/Agent/Index.php new file mode 100644 index 0000000..8104c4b --- /dev/null +++ b/src/app/Livewire/Agent/Index.php @@ -0,0 +1,433 @@ + 'handleAgentResult', + ]; + + public function mount(): void + { + $this->user = auth()->user(); + + $this->subscription = $this->user->subscription()->with('plan')->first() + ?? $this->user->latestSubscription()->with('plan')->first(); + + $this->plan = $this->subscription?->plan; + $this->limit = $this->user->effective_limit; + + $this->recalcUsage(); + } + + public function send(string $text = '', bool $withAudio = false): ?array + { + $userMessage = trim($text ?: $this->message); + if (!$userMessage) return null; + + $this->reset('message'); + + $hardLimit = (int) ceil($this->limit * 1.05); + + // Stufe 1: Hart blockieren ab 105% + if ($this->limit > 0 && $this->usage >= $hardLimit) { + $this->dispatch('notify', [ + 'type' => 'error', + 'title' => 'Limit erreicht', + 'message' => 'Dein monatliches Kontingent ist aufgebraucht. Upgrade auf Pro oder warte bis zum nΓ€chsten Monat.', + ]); + return null; + } + + // Stufe 2: Warnung bei 100%-105% β€” Anfrage lΓ€uft durch + if ($this->limit > 0 && $this->usage >= $this->limit) { + $this->dispatch('notify', [ + 'type' => 'warning', + 'title' => 'Fast aufgebraucht', + 'message' => 'Du nutzt deine Toleranzreserve. Dein Kontingent ist bald erschΓΆpft.', + ]); + } + + $startTime = microtime(true); + + $aiConfig = $this->plan?->ai_config ?? []; + if (is_string($aiConfig)) { + $aiConfig = json_decode($aiConfig, true); + } + + $model = $aiConfig['model'] ?? 'gpt-4o-mini'; + + $historyForAI = array_slice($this->conversation, -8); + $messagesForAI = array_merge($historyForAI, [ + ['role' => 'user', 'content' => $userMessage], + ]); + + // User-Kontext (geteilter Service β†’ Web + API identisch) + $userContext = app(AgentContextService::class)->build($this->user); + \Log::info('UserContext LΓ€nge: ' . str_word_count($userContext) . ' WΓΆrter, ' . strlen($userContext) . ' Zeichen'); + + $parsed = AgentAIService::chat($messagesForAI, $aiConfig, $userContext); + $usage = $parsed['_usage'] ?? []; + + $this->conversationPromptTokens += $usage['prompt_tokens'] ?? 0; + $this->conversationCompletionTokens += $usage['completion_tokens'] ?? 0; + $this->conversationTotalTokens += $usage['total_tokens'] ?? 0; + $this->conversationCostUsd += $this->calculateCost($usage, $model); + + try { + // ── Multi-Action: Array von Aktionen ────────────────────────── + if (isset($parsed['_multi'])) { + $actions = $parsed['_multi']; + $results = []; + $messages = []; + + foreach ($actions as $action) { + if (!isset($action['type'])) continue; + $result = AgentActionService::handle($this->user, $action); + $results[] = $result; + + if ($result['status'] === 'success') { + $messages[] = $result['message'] ?? 'Erledigt'; + + if (in_array($action['type'], ['event', 'event_update'])) { + $this->dispatch('eventCreated'); + } + } + } + + $duration = round((microtime(true) - $startTime) * 1000); + $credits = $this->calculateCredits(['type' => 'multi'], $duration, $usage); + + $combinedResult = [ + 'status' => 'success', + 'message' => implode(' | ', $messages) ?: 'Erledigt!', + 'meta' => ['actions' => count($actions)], + ]; + + $this->logConversationAction($userMessage, ['type' => 'multi'], $combinedResult, $model, $duration, $credits); + + $assistantMsg = implode('. ', $messages) . '. Kann ich noch etwas fΓΌr dich tun?'; + $this->conversation[] = ['role' => 'user', 'content' => $userMessage]; + $this->conversation[] = ['role' => 'assistant', 'content' => $assistantMsg]; + + if (str_contains($assistantMsg, '[END]')) { + $this->dispatch('conversation-ended'); + } + + $this->applyResult($combinedResult); + $this->dispatch('agent:sent'); + + } elseif (($parsed['type'] ?? 'unknown') === 'chat') { + // ── Chat-Antwort: Log nur bei [END] ohne vorherige Aktion ── + $chatMessage = $parsed['data']['message'] ?? 'Hmm, da bin ich ΓΌberfragt.'; + + $this->conversation[] = ['role' => 'user', 'content' => $userMessage]; + $this->conversation[] = ['role' => 'assistant', 'content' => $chatMessage]; + + if (str_contains($chatMessage, '[END]')) { + // Gab es in dieser Session bereits eine geloggte Aktion? + $hadAction = collect($this->conversation) + ->where('role', 'assistant') + ->filter(fn($msg) => !str_contains($msg['content'], '[END]')) + ->count() > 1; + + if (!$hadAction) { + $duration = round((microtime(true) - $startTime) * 1000); + $this->logConversationAction( + collect($this->conversation) + ->where('role', 'user') + ->first()['content'] ?? $userMessage, + ['type' => 'chat', 'data' => []], + ['status' => 'success', 'message' => 'Chat', 'meta' => []], + $model, + $duration, + 5 + ); + } + + $this->dispatch('conversation-ended'); + } + + $this->lastAction = null; + + } else { + // ── Einzelne Aktion (event, note, task) ─────────────────────── + $result = AgentActionService::handle($this->user, $parsed); + $duration = round((microtime(true) - $startTime) * 1000); + + // Mehrdeutig β†’ AI soll nachfragen (als Chat-Antwort behandeln) + if ($result['status'] === 'ambiguous') { + $this->conversation[] = ['role' => 'user', 'content' => $userMessage]; + $this->conversation[] = ['role' => 'assistant', 'content' => $result['message']]; + $this->lastAction = null; + + } elseif ($result['status'] === 'conflict') { + $this->logConversationAction($userMessage, $parsed, $result, $model, $duration, 0); + + $this->conversation[] = ['role' => 'user', 'content' => $userMessage]; + $this->conversation[] = ['role' => 'assistant', 'content' => 'Es gibt einen Terminkonflikt. Ich zeige dir die Details.']; + + $this->dispatch('openModal', 'agent.modals.conflict-modal', [ + 'data' => $parsed['data'], + 'meta' => $result['meta'], + 'originalInput' => $userMessage, + ]); + + $this->dispatch('agent:sent'); + + } else { + $credits = $this->calculateCredits($parsed, $duration, $usage); + $this->logConversationAction($userMessage, $parsed, $result, $model, $duration, $credits); + + if ($result['status'] === 'success' && ($parsed['type'] ?? '') === 'event') { + $this->dispatch('eventCreated'); + + Activity::log( + $this->user->id, + Activity::TYPE_EVENT_CREATED, + Activity::localizedTitle('event_created_assistant', $this->user->locale ?? 'de'), + $result['meta']['title'] ?? null, + meta: [ + 'start' => $result['meta']['start'] ?? null, + 'credits' => $credits, + 'duration' => $result['meta']['duration'] ?? null, + ] + ); + } + + // NatΓΌrliche BestΓ€tigung + $confirmMsg = $result['status'] === 'success' + ? ($result['message'] ?? 'Erledigt!') . ' Kann ich noch etwas fΓΌr dich tun?' + : ($result['message'] ?? 'Da hat etwas nicht geklappt.'); + + $this->conversation[] = ['role' => 'user', 'content' => $userMessage]; + $this->conversation[] = ['role' => 'assistant', 'content' => $confirmMsg]; + + if (str_contains($confirmMsg, '[END]')) { + $this->dispatch('conversation-ended'); + } + + $this->applyResult($result); + } + } + + } catch (\Throwable $e) { + $duration = round((microtime(true) - $startTime) * 1000); + + $this->logConversationAction($userMessage, $parsed, [ + 'status' => 'failed', + 'meta' => ['error' => $e->getMessage()], + ], $model, $duration, 0); + + $this->conversation[] = ['role' => 'user', 'content' => $userMessage]; + $this->conversation[] = ['role' => 'assistant', 'content' => 'Da ist leider etwas schiefgelaufen.']; + + $this->applyResult([ + 'status' => 'failed', + 'message' => 'Fehler bei der Verarbeitung', + 'meta' => [], + ]); + } + + $this->dispatch('agent:sent'); + + // TTS im selben Request mitsynthetisieren (spart einen Round-Trip) + if ($withAudio && !empty($this->conversation)) { + $lastMsg = end($this->conversation); + if ($lastMsg['role'] === 'assistant') { + $spokenText = trim(str_replace('[END]', '', $lastMsg['content'])); + if ($spokenText) { + $audio = AgentAIService::textToSpeech($spokenText, $aiConfig); + return ['audio' => $audio]; + } + } + } + + return null; + } + + public function synthesize(string $text): array + { + $aiConfig = $this->plan?->ai_config ?? []; + if (is_string($aiConfig)) { + $aiConfig = json_decode($aiConfig, true); + } + + $audio = AgentAIService::textToSpeech($text, $aiConfig); + + return ['audio' => $audio]; + } + + public function clearConversation(): void + { + $this->conversation = []; + $this->lastAction = null; + $this->conversationPromptTokens = 0; + $this->conversationCompletionTokens = 0; + $this->conversationTotalTokens = 0; + $this->conversationCostUsd = 0; + } + + public function handleAgentResult(array $result): void + { + $this->applyResult($result); + } + + public function render() + { + return view('livewire.agent.index')->layout('layouts.app'); + } + + // ── Private ─────────────────────────────────────────────────────────── + + private function extractChatTitle(string $userMessage): string + { + $farewells = [ + 'nein danke', 'nein', 'danke', 'passt', 'das wars', + 'ok danke', 'okay danke', 'super danke', 'alles klar', + 'nichts mehr', 'tschΓΌss', 'bye', 'ciao', + ]; + + $isGoodbye = in_array(strtolower(trim($userMessage)), $farewells); + + if ($isGoodbye && count($this->conversation) > 0) { + foreach ($this->conversation as $msg) { + if ($msg['role'] === 'user') { + return mb_substr($msg['content'], 0, 80); + } + } + } + + return mb_substr($userMessage, 0, 80); + } + + private function logConversationAction( + string $userMessage, + array $parsed, + array $result, + string $model, + int $duration, + int $credits, + ): void { + $input = match ($parsed['type']) { + 'event', 'event_update' => $parsed['data']['title'] ?? $userMessage, + 'note', 'note_update' => mb_substr($parsed['data']['content'] ?? $userMessage, 0, 100), + 'task', 'task_update' => $parsed['data']['title'] ?? $userMessage, + 'contact' => $parsed['data']['name'] ?? $userMessage, + 'email' => 'E-Mail an ' . ($parsed['data']['contact'] ?? '') . ': ' . mb_substr($parsed['data']['subject'] ?? '', 0, 50), + 'multi' => $userMessage, + 'chat' => $this->extractChatTitle($userMessage), + default => mb_substr($userMessage, 0, 200), + }; + + AgentLog::create([ + 'user_id' => $this->user->id, + 'type' => $parsed['type'], + 'input' => $input, + 'status' => $result['status'], + 'output' => $result['meta'] ?? null, + 'credits' => $credits, + 'ai_response' => $parsed, + 'model' => $model, + 'duration_ms' => $duration, + 'prompt_tokens' => $this->conversationPromptTokens, + 'completion_tokens' => $this->conversationCompletionTokens, + 'total_tokens' => $this->conversationTotalTokens, + 'cost_usd' => $this->conversationCostUsd, + ]); + + $this->recalcUsage(); + + if ($this->limit > 0 && $this->usage >= $this->limit) { + $this->dispatch('notify', [ + 'type' => 'warning', + 'title' => 'Kontingent aufgebraucht', + 'message' => 'Dein Kontingent ist erschΓΆpft. Upgrade auf Pro oder warte bis zum nΓ€chsten Monat.', + ]); + } + } + + private function applyResult(array $result): void + { + $this->lastAction = [ + 'status' => $result['status'], + 'message' => $result['message'], + 'meta' => $result['meta'] ?? [], + 'time' => now(), + ]; + + $this->recalcUsage(); + } + + private function recalcUsage(): void + { + $this->user->refresh(); + + if ($this->user->effective_limit === 0) { + $this->usage = 0; + $this->usagePercent = 0; + $this->limit = 0; + return; + } + + $this->limit = $this->user->effective_limit; + $this->usage = $this->user->effective_usage; + $this->usagePercent = $this->user->usage_percent; + } + + private function calculateCost(array $usage, string $model): float + { + $aiConfig = $this->plan?->ai_config ?? []; + if (is_string($aiConfig)) { + $aiConfig = json_decode($aiConfig, true); + } + + $inputCost = ($usage['prompt_tokens'] ?? 0) / 1000 * ($aiConfig['input_cost'] ?? 0.00015); + $outputCost = ($usage['completion_tokens'] ?? 0) / 1000 * ($aiConfig['output_cost'] ?? 0.0006); + + return round($inputCost + $outputCost, 6); + } + +private function calculateCredits(array $parsed, int $duration, array $usage): int + { + // Flat-Basis + Output-Tokens. Kontext (prompt_tokens) wird ignoriert, + // damit Credits nicht mit Kalendergrâße skalieren. Cap bei 100. + $completionTokens = (int) ($usage['completion_tokens'] ?? 0); + $credits = 20 + (int) ceil($completionTokens * 0.3); + + return min(100, max(1, $credits)); + } +} diff --git a/src/app/Livewire/Agent/Logs.php b/src/app/Livewire/Agent/Logs.php new file mode 100644 index 0000000..b650ed2 --- /dev/null +++ b/src/app/Livewire/Agent/Logs.php @@ -0,0 +1,65 @@ +resetPage(); } + public function updatedFilterStatus(): void { $this->resetPage(); } + + public function render() + { + $userId = auth()->id(); + $tz = auth()->user()->timezone ?? 'UTC'; + + $query = AgentLog::where('user_id', $userId)->latest(); + + if ($this->search !== '') { + $query->where('input', 'like', '%' . $this->search . '%'); + } + + if ($this->filterStatus !== '') { + $query->where('status', $this->filterStatus); + } + + $logs = $query->paginate(25); + + // Monats-Statistiken + $stats = AgentLog::where('user_id', $userId) + ->where('created_at', '>=', now()->startOfMonth()) + ->selectRaw(" + count(*) as total, + sum(credits) as credits_used, + sum(cost_usd) as total_cost, + sum(total_tokens) as total_tokens, + sum(case when status = 'success' then 1 else 0 end) as successes, + sum(case when status = 'failed' then 1 else 0 end) as failures, + avg(duration_ms) as avg_duration + ") + ->first(); + + // Credit-Limit fΓΌr Fortschrittsanzeige + $user = auth()->user(); + $limit = $user->effective_limit; + $usage = $user->effective_usage; + $usagePercent = $user->usage_percent; + + return view('livewire.agent.logs', [ + 'logs' => $logs, + 'stats' => $stats, + 'tz' => $tz, + 'usage' => $usage, + 'limit' => $limit, + 'usagePercent' => $usagePercent, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Agent/Modals/ConflictModal.php b/src/app/Livewire/Agent/Modals/ConflictModal.php new file mode 100644 index 0000000..838b981 --- /dev/null +++ b/src/app/Livewire/Agent/Modals/ConflictModal.php @@ -0,0 +1,139 @@ +data = $data; + $this->meta = $meta; + $this->originalInput = $originalInput; + } + + public function selectSlot($type) + { + $this->selectedSlot = $this->selectedSlot === $type ? null : $type; + } + + public function save() + { + // πŸ‘‰ SLOT ÜBERNEHMEN + if ($this->selectedSlot === 'prev' && !empty($this->meta['suggestion_prev'])) { + $this->data['datetime'] = $this->meta['suggestion_prev']; + $this->data['duration_minutes'] = $this->meta['duration']; + } elseif ($this->selectedSlot === 'next' && !empty($this->meta['suggestion_next'])) { + $this->data['datetime'] = $this->meta['suggestion_next']; + $this->data['duration_minutes'] = $this->meta['duration']; + } + + // πŸ‘‰ IMMER force = true (weil Konflikt bewusst) + $this->data['force'] = true; + + $result = AgentActionService::handle(auth()->user(), [ + 'type' => 'event', + 'data' => $this->data, + ]); + + if ($result['status'] === 'success') { + $this->dispatch('eventCreated'); + + Activity::log( + auth()->id(), + Activity::TYPE_EVENT_CREATED, + Activity::localizedTitle('event_created_assistant', auth()->user()->locale ?? 'de'), + $result['meta']['title'] ?? null, + meta: ['start' => $result['meta']['start'] ?? null] + ); + } + + $this->dispatch('agent:result', $result); + $this->dispatch('closeModal'); + } + + public function render() + { + return view('livewire.agent.modals.conflict-modal'); + } +} + +// +//namespace App\Livewire\Agent\Modals; +// +//use App\Services\AgentActionService; +//use LivewireUI\Modal\ModalComponent; +// +//class ConflictModal extends ModalComponent +//{ +// public $data = []; +// public $meta = []; +// public $originalInput = ''; +// public $selectedSlot = null; // 'prev' | 'next' | null +// +// public function mount($data, $meta, $originalInput) +// { +// $this->data = $data; +// $this->meta = $meta; +// $this->originalInput = $originalInput; +// } +// +// public function selectSlot($type) +// { +// if ($this->selectedSlot === $type) { +// // toggle off +// $this->selectedSlot = null; +// } else { +// $this->selectedSlot = $type; +// } +// } +// +// public function forceSave() +// { +// $this->data['force'] = true; +// +// $result = AgentActionService::handle(auth()->user(), [ +// 'type' => 'event', +// 'data' => $this->data +// ]); +// +// $this->dispatch('agent:result', $result); +// $this->dispatch('closeModal'); +// } +// +// public function acceptSuggestion($type) +// { +// if ($type === 'next' && !empty($this->meta['suggestion_next'])) { +// $this->data['start'] = $this->meta['suggestion_next']; +// } +// +// if ($type === 'prev' && !empty($this->meta['suggestion_prev'])) { +// $this->data['start'] = $this->meta['suggestion_prev']; +// } +// +// $this->data['force'] = true; +// +// $result = AgentActionService::handle(auth()->user(), [ +// 'type' => 'event', +// 'data' => $this->data +// ]); +// +// $this->dispatch('agent:result', $result); +// $this->dispatch('closeModal'); +// } +// +// public function render() +// { +// return view('livewire.agent.modals.conflict-modal'); +// } +//} diff --git a/src/app/Livewire/Auth/ForgotPassword.php b/src/app/Livewire/Auth/ForgotPassword.php new file mode 100644 index 0000000..29b67b2 --- /dev/null +++ b/src/app/Livewire/Auth/ForgotPassword.php @@ -0,0 +1,50 @@ +validate([ + 'email' => 'required|email', + ]); + + $user = User::where('email', $this->email)->first(); + + if (!$user) { + // Gleiche Meldung aus Security-GrΓΌnden + $this->sent = true; + return; + } + + $token = Str::random(64); + + DB::table('password_reset_tokens')->updateOrInsert( + ['email' => $this->email], + ['token' => Hash::make($token), 'created_at' => now()] + ); + + Mail::mailer('system')->to($this->email)->send(new ResetPasswordMail($user, $token)); + + $this->sent = true; + } + + public function render() + { + return view('livewire.auth.forgot-password') + ->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Auth/Login.php b/src/app/Livewire/Auth/Login.php new file mode 100644 index 0000000..b537790 --- /dev/null +++ b/src/app/Livewire/Auth/Login.php @@ -0,0 +1,55 @@ +validate( + [ + 'email' => ['required', 'email'], + 'password' => ['required'], + ], + [ + 'email.required' => t('auth.validation.email_required'), + 'email.email' => t('auth.validation.email_invalid'), + 'password.required' => t('auth.validation.password_required'), + ] + ); + + if (!Auth::attempt([ + 'email' => $this->email, + 'password' => $this->password + ])) { + $this->addError('email', t('auth.login.invalid_credentials')); + return; + } + + $user = Auth::user(); + + if (!$user->email_verified_at) { + Auth::logout(); // πŸ”₯ wichtig! + return redirect()->route('verify.notice', [ + 'user' => $user->id + ]); + } + + request()->session()->regenerate(); + + return redirect()->route('dashboard.index'); + } + + public function render() + { + return view('livewire.auth.login') + ->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Auth/Modals/VerifyModal.php b/src/app/Livewire/Auth/Modals/VerifyModal.php new file mode 100644 index 0000000..f9fb618 --- /dev/null +++ b/src/app/Livewire/Auth/Modals/VerifyModal.php @@ -0,0 +1,40 @@ +user = User::findOrFail($userId); + } + + public function verify() + { + if (!VerificationService::verify($this->user, $this->code)) { + $this->addError('code', 'UngΓΌltiger Code'); + return; + } + + $this->dispatch('closeModal'); + + return redirect()->route('dashboard.index'); + } + + public function resend() + { + VerificationService::create($this->user); + } + + public function render() + { + return view('livewire.auth.modals.verify-modal'); + } +} diff --git a/src/app/Livewire/Auth/Register.php b/src/app/Livewire/Auth/Register.php new file mode 100644 index 0000000..c5906e7 --- /dev/null +++ b/src/app/Livewire/Auth/Register.php @@ -0,0 +1,94 @@ +validate( + [ + 'name' => ['required'], + 'email' => ['required', 'email', 'unique:users,email'], + 'password' => ['required', 'min:6'], + ], + [ + // NAME + 'name.required' => t('auth.validation.name_required'), + + // EMAIL + 'email.required' => t('auth.validation.email_required'), + 'email.email' => t('auth.validation.email_invalid'), + 'email.unique' => t('auth.validation.email_taken'), + + // PASSWORD + 'password.required' => t('auth.validation.password_required'), + 'password.min' => t('auth.validation.password_min'), + ] + ); + + // πŸ”₯ Browser Sprache holen + $locale = substr(request()->header('Accept-Language'), 0, 2); + $locale = in_array($locale, array_keys(config('app.locales'))) + ? $locale + : config('app.default_locale'); + + // πŸ”₯ Timezone absichern + if (!in_array($this->timezone, timezone_identifiers_list())) { + $this->timezone = 'UTC'; + } + + $user = User::create([ + 'name' => $this->name, + 'email' => $this->email, + 'password' => Hash::make($this->password), + 'locale' => $locale, + 'timezone' => $this->timezone, + ]); + + $plan = Plan::where('name', 'Free') + ->where('active', true) + ->firstOrFail(); + + // SUBSCRIPTION ANLEGEN (eine Zeile pro User, wird bei Planwechsel nur geupdated) + Subscription::create([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'plan_slug' => $plan->plan_key ?? Str::slug($plan->name), + 'price' => $plan->price ?? 0, + 'interval' => $plan->interval ?? 'monthly', + 'status' => 'active', + 'starts_at' => now(), + 'provider' => null, + ]); + + VerificationService::create($user, 'email'); + + return redirect()->route('verify.notice', [ + 'user' => $user->id + ]); + } + + public function render() + { + return view('livewire.auth.register') + ->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Auth/ResetPassword.php b/src/app/Livewire/Auth/ResetPassword.php new file mode 100644 index 0000000..3cb581a --- /dev/null +++ b/src/app/Livewire/Auth/ResetPassword.php @@ -0,0 +1,61 @@ +token = $token; + $this->email = request()->query('email', ''); + } + + public function resetPassword() + { + $this->validate([ + 'password' => 'required|min:8|confirmed', + ]); + + $record = DB::table('password_reset_tokens') + ->where('email', $this->email) + ->first(); + + if (!$record || !Hash::check($this->token, $record->token)) { + $this->error = 'UngΓΌltiger oder abgelaufener Link.'; + return; + } + + if (Carbon::parse($record->created_at)->diffInMinutes(now()) > 60) { + $this->error = 'Link abgelaufen. Bitte neu anfordern.'; + return; + } + + User::where('email', $this->email) + ->update(['password' => Hash::make($this->password)]); + + DB::table('password_reset_tokens') + ->where('email', $this->email) + ->delete(); + + session()->flash('success', 'Passwort erfolgreich geΓ€ndert.'); + $this->redirect('/login', navigate: true); + } + + public function render() + { + return view('livewire.auth.reset-password') + ->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Auth/Verify.php b/src/app/Livewire/Auth/Verify.php new file mode 100644 index 0000000..84b031b --- /dev/null +++ b/src/app/Livewire/Auth/Verify.php @@ -0,0 +1,125 @@ +user = User::findOrFail($user); + } + + public function updatedCode() + { + if (strlen(implode('', $this->code)) === 6) { + $this->verify(); + } + } + + public function tick() + { + if ($this->cooldown > 0) { + $this->cooldown--; + } + } + + public function resend() + { + $user = $this->user; + + // πŸ”₯ Reset nach 1 Stunde + if ( + $user->verification_resends_reset_at && + now()->gt($user->verification_resends_reset_at) + ) { + $user->update([ + 'verification_resends' => 0, + 'verification_resends_reset_at' => now()->addHour(), + ]); + } + + // πŸ”₯ Initial setzen (beim ersten Mal) + if (!$user->verification_resends_reset_at) { + $user->verification_resends_reset_at = now()->addHour(); + } + + // πŸ”₯ Limit prΓΌfen + if ($user->verification_resends >= 5) { + + $this->dispatch('notify', [ + 'type' => 'error', + 'title' => t('auth.verify.limit_title'), + 'message' => t('auth.verify.limit_message'), + ]); + + return; + } + + // πŸ”₯ Code neu generieren + $code = rand(100000, 999999); + + $user->update([ + 'email_verification_code' => $code, + 'email_verification_expires_at' => now()->addMinutes(10), + + 'verification_resends' => $user->verification_resends + 1, + 'verification_resends_reset_at' => $user->verification_resends_reset_at ?? now()->addHour(), + ]); + + MailService::queue( + $user->email, + 'auth.verify', + [ + 'user' => $user->name, + 'code' => $code, + ], + $user->locale, + null, + $user->id, + ); + + $this->cooldown = 30; + } + + + public function verify() + { + $code = implode('', $this->code); + + if ($code !== $this->user->email_verification_code) { + $this->addError('code', t('auth.verify.invalid')); + return; + } + + if (now()->gt($this->user->email_verification_expires_at)) { + $this->addError('code', t('auth.verify.expired')); + return; + } + + $this->user->update([ + 'email_verified_at' => now(), + 'email_verification_code' => null, + 'email_verification_expires_at' => null, + 'verification_resends' => 0, + 'verification_resends_reset_at' => null, + ]); + + session()->flash('success', t('auth.verify.success')); + + return redirect()->route('login'); + } + + public function render() + { + return view('livewire.auth.verify') + ->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Auth/VerifyNotice.php b/src/app/Livewire/Auth/VerifyNotice.php new file mode 100644 index 0000000..556735d --- /dev/null +++ b/src/app/Livewire/Auth/VerifyNotice.php @@ -0,0 +1,422 @@ +user = User::findOrFail($user); + + if ($this->user->email_verified_at) { + return redirect()->route('dashboard.index'); + } + + // πŸ”₯ aktuelle verification holen + $this->verification = Verification::where('user_id', $this->user->id) + ->where('type', 'email') + ->whereNull('verified_at') + ->latest() + ->first(); + + // πŸ”₯ COOLDOWN (Redis) + $key = "verify:cooldown:{$this->user->id}"; + $expiresAtRaw = Cache::get($key); + + if ($expiresAtRaw) { + $expiresAt = Carbon::parse($expiresAtRaw); + + $this->cooldownUntil = $expiresAt + ->copy() + ->timezone($this->user->timezone); + + $this->cooldown = max(0, floor(now()->diffInSeconds($expiresAt, false))); + } + + // πŸ”₯ BLOCK (aus verification) + if ( + $this->verification && + $this->verification->resends >= 5 + ) { + $this->blockedUntil = $this->verification->resends_reset_at + ? $this->verification->resends_reset_at + ->copy() + ->timezone($this->user->timezone) + : null; + } + } + + public function tick() + { + if ($this->cooldown > 0) { + $this->cooldown--; + } + } + + public function resend() + { + if ($this->cooldown > 0) return; + + $verification = $this->verification; + + // πŸ”₯ wenn keine existiert β†’ neu erstellen + if (!$verification) { + $this->verification = VerificationService::create($this->user); + return; + } + + // πŸ”₯ RESET nach 1h + if ( + $verification->resends_reset_at && + now()->gt($verification->resends_reset_at) + ) { + $verification->update([ + 'resends' => 0, + 'resends_reset_at' => now()->addHour(), + ]); + } + + // πŸ”₯ INIT + if (!$verification->resends_reset_at) { + $verification->update([ + 'resends_reset_at' => now()->addHour(), + ]); + } + + // πŸ”₯ LIMIT + if ($verification->resends >= 5) { + $this->dispatch('notify', [ + 'type' => 'error', + 'title' => t('auth.verify.limit_title'), + 'message' => t('auth.verify.limit_message'), + ]); + return; + } + + // πŸ”₯ NEUE VERIFICATION (inkl. Mail + URL) + $this->verification = VerificationService::create($this->user); + + // πŸ”₯ COUNTER erhΓΆhen (auf alter verification!) + $verification->increment('resends'); + + // πŸ”₯ COOLDOWN + $expiresAt = now()->addSeconds(30); + + Cache::put( + "verify:cooldown:{$this->user->id}", + $expiresAt->toDateTimeString(), + $expiresAt + ); + + $this->cooldown = 30; + $this->cooldownUntil = $expiresAt + ->copy() + ->timezone($this->user->timezone); + } + + public function render() + { + return view('livewire.auth.verify-notice') + ->layout('layouts.blank'); + } +} +// +//namespace App\Livewire\Auth; +// +//use App\Models\User; +//use App\Services\MailService; +//use Illuminate\Support\Facades\Cache; +//use Illuminate\Support\Facades\URL; +//use Livewire\Component; +//use Carbon\Carbon; +// +//class VerifyNotice extends Component +//{ +// public $cooldown = 0; +// public $user; +// public $blockedUntil; +// public $cooldownUntil; +// +// public function mount($user) +// { +// $this->user = User::findOrFail($user); +// +// if ($this->user->email_verified_at) { +// return redirect()->route('dashboard.index'); +// } +// +// $key = "verify:cooldown:{$this->user->id}"; +// +// $expiresAtRaw = Cache::get($key); +// +// if ($expiresAtRaw) { +// +// // πŸ”₯ STRING β†’ CARBON +// $expiresAt = Carbon::parse($expiresAtRaw); +// +// $this->cooldownUntil = $expiresAt +// ->copy() +// ->timezone($this->user->timezone); +// +// $this->cooldown = max(0, floor(now()->diffInSeconds($expiresAt, false))); +// } +// +// // πŸ”₯ BLOCKED +// if ($this->user->verification_resends >= 5) { +// $this->blockedUntil = $this->user->verification_resends_reset_at +// ? $this->user->verification_resends_reset_at +// ->copy() +// ->timezone($this->user->timezone) +// : null; +// } +// } +// +// public function tick() +// { +// if ($this->cooldown > 0) { +// $this->cooldown--; +// } +// } +// +// public function resend() +// { +// if ($this->cooldown > 0) return; +// +// $user = $this->user; +// $key = "verify:cooldown:{$user->id}"; +// +// // πŸ”₯ RESET (1h) +// if ( +// $user->verification_resends_reset_at && +// now()->gt($user->verification_resends_reset_at) +// ) { +// $user->update([ +// 'verification_resends' => 0, +// 'verification_resends_reset_at' => now()->addHour(), +// ]); +// } +// +// // πŸ”₯ INIT +// if (!$user->verification_resends_reset_at) { +// $user->update([ +// 'verification_resends_reset_at' => now()->addHour() +// ]); +// } +// +// // πŸ”₯ LIMIT +// if ($user->verification_resends >= 5) { +// $this->dispatch('notify', [ +// 'type' => 'error', +// 'title' => t('auth.verify.limit_title'), +// 'message' => t('auth.verify.limit_message'), +// ]); +// return; +// } +// +// // πŸ”₯ SIGNED URL +// $url = URL::temporarySignedRoute( +// 'verify.user', +// now()->addMinutes(10), +// ['user' => $user->id] +// ); +// +// // πŸ”₯ MAIL +// MailService::queue( +// $user->email, +// 'auth.verify', +// [ +// 'user' => $user->name, +// 'url' => $url, +// ], +// $user->locale, +// null, +// $user->id +// ); +// +// // πŸ”₯ COUNTER +// $user->increment('verification_resends'); +// +// // πŸ”₯ COOLDOWN +// $expiresAt = now()->addSeconds(30); +// +// // πŸ‘‰ STRING speichern! +// Cache::put($key, $expiresAt->toDateTimeString(), $expiresAt); +// +// $this->cooldown = 30; +// +// $this->cooldownUntil = $expiresAt +// ->copy() +// ->timezone($user->timezone); +// } +// +// public function render() +// { +// return view('livewire.auth.verify-notice') +// ->layout('layouts.blank'); +// } +//} + +// +//namespace App\Livewire\Auth; +// +//use App\Models\User; +//use App\Services\MailService; +//use Illuminate\Support\Facades\Cache; +//use Illuminate\Support\Facades\URL; +//use Livewire\Component; +// +//class VerifyNotice extends Component +//{ +// public $cooldown = 0; +// public $user; +// public $blockedUntil; +// public $cooldownUntil; +// +// public function mount($user) +// { +// $this->user = User::findOrFail($user); +// +// $key = "verify:cooldown:{$this->user->id}"; +// +// if (Cache::has($key)) { +// $expiresAt = \Carbon\Carbon::parse(Cache::get($key)); +// +// $this->cooldownUntil = $expiresAt +// ->copy() +// ->timezone($this->user->timezone); +// +// $this->cooldown = now()->diffInSeconds($expiresAt, false); +// +// if ($this->cooldown < 0) { +// $this->cooldown = 0; +// } +// } +// +// if ($this->user->verification_resends >= 5) { +// $this->blockedUntil = $this->user->verification_resends_reset_at +// ? \Carbon\Carbon::parse($this->user->verification_resends_reset_at) +// ->timezone($this->user->timezone) +// : null; +// } +// } +// +// public function tick() +// { +// if ($this->cooldown > 0) { +// $this->cooldown--; +// } +// } +// +// public function resend() +// { +// if ($this->cooldown > 0) return; +// +// $user = $this->user; +// $key = "verify:cooldown:{$user->id}"; +// +// // πŸ”₯ Reset nach 1 Stunde +// if ( +// $user->verification_resends_reset_at && +// now()->gt($user->verification_resends_reset_at) +// ) { +// $user->update([ +// 'verification_resends' => 0, +// 'verification_resends_reset_at' => now()->addHour(), +// ]); +// } +// +// // πŸ”₯ Initial setzen +// if (!$user->verification_resends_reset_at) { +// $user->update([ +// 'verification_resends_reset_at' => now()->addHour() +// ]); +// } +// +// // πŸ”₯ LIMIT +// if ($user->verification_resends >= 5) { +// $this->dispatch('notify', [ +// 'type' => 'error', +// 'title' => t('auth.verify.limit_title'), +// 'message' => t('auth.verify.limit_message'), +// ]); +// return; +// } +// +// // πŸ”₯ URL generieren +// $url = URL::temporarySignedRoute( +// 'verify.user', +// now()->addMinutes(10), +// ['user' => $user->id] +// ); +// +// // πŸ”₯ Queue +// MailService::queue( +// $user->email, +// 'auth.verify', +// [ +// 'user' => $user->name, +// 'url' => $url, +// ], +// $user->locale, +// null, +// $user->id +// ); +// +// // πŸ”₯ Counter erhΓΆhen +// $user->increment('verification_resends'); +// +// $expiresAt = now()->addSeconds(30); +// +// Cache::put($key, now()->addSeconds(30), now()->addSeconds(30)); +// +// $this->cooldown = 30; +// $this->cooldownUntil = $expiresAt; +// } +// +// +//// public function resend() +//// { +//// if ($this->cooldown > 0) return; +//// +//// $user = $this->user; +//// +//// $url = URL::temporarySignedRoute( +//// 'verify.user', +//// now()->addMinutes(10), +//// ['user' => $user->id] +//// ); +//// +//// MailService::queue( +//// $user->email, +//// 'auth.verify', +//// [ +//// 'user' => $user->name, +//// 'url' => $url, +//// ], +//// $user->locale, +//// null, +//// $user->id +//// ); +//// +//// $this->cooldown = 30; +//// } +// +// public function render() +// { +// return view('livewire.auth.verify-notice') +// ->layout('layouts.blank'); +// } +//} diff --git a/src/app/Livewire/Automation/Index.php b/src/app/Livewire/Automation/Index.php new file mode 100644 index 0000000..dc347ed --- /dev/null +++ b/src/app/Livewire/Automation/Index.php @@ -0,0 +1,168 @@ +activeType = $type; + $automation = $this->getRecord($type); + $cfg = $automation?->config ?? $types[$type]['defaults'] ?? []; + + $this->f_minutes_before = (string) ($cfg['minutes_before'] ?? 30); + $this->f_send_time = $cfg['send_time'] ?? '08:00'; + $this->f_send_day = (string) ($cfg['send_day'] ?? 1); + $this->f_delay_minutes = (string) ($cfg['delay_minutes'] ?? 30); + $this->f_days_before = (string) ($cfg['days_before'] ?? 7); + $this->f_days_inactive = (string) ($cfg['days_inactive'] ?? 3); + $this->f_days_since_contact = (string) ($cfg['days_since_contact'] ?? 14); + $this->f_min_slot_minutes = (string) ($cfg['min_slot_minutes'] ?? 60); + + $this->panelOpen = true; + } + + public function closePanel(): void + { + $this->panelOpen = false; + $this->activeType = ''; + } + + public function save(): void + { + $types = config('automations.types'); + $type = $this->activeType; + if (!isset($types[$type])) return; + + $user = auth()->user(); + $def = $types[$type]; + $isPro = $user->isInternalUser() || $user->hasFeature('automations'); + $isFreeType = (bool) ($def['is_free'] ?? false); + + if (!$isFreeType && !$isPro) { + $this->closePanel(); + return; + } + + Automation::updateOrCreate( + ['user_id' => $user->id, 'type' => $type], + [ + 'name' => $def['name'], + 'active' => true, + 'config' => $this->buildConfig($type), + ] + ); + + $this->closePanel(); + } + + public function toggle(string $type): void + { + $types = config('automations.types'); + if (!isset($types[$type])) return; + + $user = auth()->user(); + $def = $types[$type]; + $isPro = $user->isInternalUser() || $user->hasFeature('automations'); + $isFreeType = (bool) ($def['is_free'] ?? false); + + if (!$isFreeType && !$isPro) return; + + $record = $this->getRecord($type); + + if ($record) { + $record->update(['active' => !$record->active]); + } else { + Automation::create([ + 'user_id' => $user->id, + 'type' => $type, + 'name' => $def['name'], + 'active' => true, + 'config' => $def['defaults'], + ]); + } + } + + public function delete(string $type): void + { + Automation::where('user_id', auth()->id()) + ->where('type', $type) + ->delete(); + } + + private function getRecord(string $type): ?Automation + { + return Automation::where('user_id', auth()->id()) + ->where('type', $type) + ->first(); + } + + private function buildConfig(string $type): array + { + return match ($type) { + 'event_reminder' => [ + 'minutes_before' => (int) $this->f_minutes_before, + ], + 'daily_agenda' => [ + 'send_time' => $this->f_send_time, + 'weekdays_only' => false, + ], + 'weekly_overview' => [ + 'send_day' => (int) $this->f_send_day, + 'send_time' => $this->f_send_time, + ], + 'event_followup' => [ + 'delay_minutes' => (int) $this->f_delay_minutes, + ], + 'birthday_reminder' => [ + 'days_before' => (int) $this->f_days_before, + ], + 'no_activity_reminder' => [ + 'days_inactive' => (int) $this->f_days_inactive, + ], + 'daily_summary' => [ + 'send_time' => $this->f_send_time, + ], + 'contact_followup' => [ + 'days_since_contact' => (int) $this->f_days_since_contact, + ], + 'free_slots_report' => [ + 'send_day' => (int) $this->f_send_day, + 'send_time' => $this->f_send_time, + 'min_slot_minutes' => (int) $this->f_min_slot_minutes, + ], + default => [], + }; + } + + public function render() + { + $user = auth()->user(); + $isPro = $user->isInternalUser() || $user->hasFeature('automations'); + $records = Automation::where('user_id', $user->id) + ->get() + ->keyBy('type'); + + return view('livewire.automation.index', [ + 'types' => config('automations.types'), + 'records' => $records, + 'isPro' => $isPro, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Calendar/Forms/EventForm.php b/src/app/Livewire/Calendar/Forms/EventForm.php new file mode 100644 index 0000000..0012580 --- /dev/null +++ b/src/app/Livewire/Calendar/Forms/EventForm.php @@ -0,0 +1,293 @@ + 'save', + 'eventForm:delete' => 'delete', + ]; + + public function mount($eventId = null, $starts_at = null, $time = null) + { + $this->eventId = $eventId; + $this->starts_at = $starts_at; + + $tz = auth()->user()->timezone ?? config('app.timezone'); + + if (!$eventId && $time) { + $this->start_time = $time; + [$h, $m] = array_map('intval', explode(':', $time)); + $endCarbon = Carbon::createFromTime($h, $m)->addHour(); + $this->end_time = $endCarbon->format('H:i'); + } + + if ($eventId) { + + $event = Event::with('contacts')->findOrFail($eventId); + + $this->title = $event->title; + + $activeDate = $starts_at + ? Carbon::parse($starts_at, $tz) + : $event->starts_at->copy()->timezone($tz); + + $this->starts_at = $activeDate->format('Y-m-d'); + + $display = $event->getDisplayTimeForDate($this->starts_at, $tz); + + $this->start_time = $display['start']?->format('H:i'); + $this->end_time = $display['end']?->format('H:i'); + + $this->is_all_day = $event->is_all_day; + $this->color = $event->color; + $this->notes = $event->notes; + $this->recurrence = $event->recurrence; + $this->recurrence_end_date = $event->recurrence_end_date?->format('Y-m-d'); + + $this->reminders = $event->reminders ?? []; + + $this->attendees = $event->contacts->map(fn($c) => [ + 'id' => $c->id, + 'name' => $c->name, + ])->values()->toArray(); + + if ($event->ends_at && !$event->starts_at->isSameDay($event->ends_at)) { + $this->is_multi_day = true; + + $this->multi_start_date = $event->starts_at + ->timezone($tz) + ->format('Y-m-d'); + + $this->multi_end_date = $event->ends_at + ->timezone($tz) + ->format('Y-m-d'); + } + + $this->exceptions = collect($event->exceptions ?? []) + ->keyBy('date') + ->toArray(); + } + } + + public function getDaysProperty() + { + if (!$this->is_multi_day) return collect(); + + return collect( + CarbonPeriod::create($this->multi_start_date, $this->multi_end_date) + ); + } + + public function updatedIsMultiDay($value) + { + if ($value && $this->starts_at) { + $this->multi_start_date = $this->starts_at; + $this->multi_end_date = Carbon::parse($this->starts_at)->addDay()->format('Y-m-d'); + } + } + + public function updatedAttendeeSearch($value) + { + if (strlen(trim($value)) < 1) { + $this->attendeeSuggestions = []; + return; + } + + $selectedIds = array_column($this->attendees, 'id'); + + $this->attendeeSuggestions = auth()->user() + ->contacts() + ->where(function ($q) use ($value) { + $q->where('name', 'like', "%{$value}%") + ->orWhere('email', 'like', "%{$value}%"); + }) + ->whereNotIn('id', $selectedIds) + ->limit(6) + ->get(['id', 'name']) + ->toArray(); + } + + public function addAttendee(string $id, string $name): void + { + $alreadyAdded = collect($this->attendees)->contains('id', $id); + if (!$alreadyAdded) { + $this->attendees[] = ['id' => $id, 'name' => $name]; + } + $this->attendeeSearch = ''; + $this->attendeeSuggestions = []; + } + + public function removeAttendee(string $id): void + { + $this->attendees = array_values( + array_filter($this->attendees, fn($a) => $a['id'] !== $id) + ); + } + + public function addCustomReminder(): void + { + if (empty($this->customReminderTime)) return; + + $this->reminders[] = [ + 'type' => $this->customReminderType ?: 'time_of_day', + 'time' => $this->customReminderTime, + ]; + + $this->customReminderTime = ''; + $this->customReminderType = 'time_of_day'; + } + + public function addReminder(string $type, ?int $minutes = null, ?string $time = null): void + { + $this->reminders[] = array_filter([ + 'type' => $type, + 'minutes' => $minutes, + 'time' => $time, + ], fn($v) => $v !== null); + } + + public function removeReminder(int $i): void + { + array_splice($this->reminders, $i, 1); + } + + public function toggleException($date) + { + if (isset($this->exceptions[$date])) { + unset($this->exceptions[$date]); + } else { + $this->exceptions[$date] = [ + 'start' => $this->start_time, + 'end' => $this->end_time, + ]; + } + } + + public function delete() + { + if (!$this->eventId) return; + + $event = Event::where('id', $this->eventId) + ->where('user_id', auth()->id()) + ->first(); + + if ($event) { + $event->contacts()->detach(); + $event->users()->detach(); + $event->delete(); + } + + $this->dispatch('eventCreated'); + $this->dispatch('sidebar:close'); + } + + public function save() + { + $tz = auth()->user()->timezone ?? 'UTC'; + + $start = Carbon::createFromFormat( + 'Y-m-d H:i', + $this->starts_at . ' ' . ($this->start_time ?? '00:00'), + $tz + )->utc(); + + $end = $this->end_time + ? Carbon::createFromFormat('Y-m-d H:i', $this->starts_at . ' ' . $this->end_time, $tz)->utc() + : ($this->is_all_day + ? Carbon::createFromFormat('Y-m-d', $this->starts_at, $tz)->endOfDay()->utc() + : null); + + if ($this->is_multi_day) { + + $start = Carbon::createFromFormat( + 'Y-m-d H:i', + $this->multi_start_date . ' ' . ($this->start_time ?? '00:00'), + $tz + )->utc(); + + $end = $this->multi_end_date + ? Carbon::createFromFormat( + 'Y-m-d H:i', + $this->multi_end_date . ' ' . ($this->end_time ?? '23:59'), + $tz + )->utc() + : null; + } + + $exceptions = collect($this->exceptions) + ->map(fn($e, $date) => [ + 'date' => $date, + 'start' => $e['start'] ?? null, + 'end' => $e['end'] ?? null, + ]) + ->values() + ->toArray(); + + $event = Event::updateOrCreate( + ['id' => $this->eventId], + [ + 'user_id' => auth()->id(), + 'title' => $this->title, + 'starts_at' => $start, + 'ends_at' => $end, + 'is_all_day' => $this->is_all_day, + 'color' => $this->color, + 'notes' => $this->notes, + 'exceptions' => $exceptions, + 'reminders' => $this->reminders, + 'recurrence' => $this->recurrence ?: null, + 'recurrence_end_date' => $this->recurrence ? ($this->recurrence_end_date ?: null) : null, + ] + ); + + $attendeeIds = array_column($this->attendees, 'id'); + $event->contacts()->sync($attendeeIds); + + $this->dispatch('eventCreated'); + $this->dispatch('sidebar:close'); + } + + public function render() + { + return view('livewire.calendar.forms.event-form'); + } +} diff --git a/src/app/Livewire/Calendar/Index.php b/src/app/Livewire/Calendar/Index.php new file mode 100644 index 0000000..472ed3c --- /dev/null +++ b/src/app/Livewire/Calendar/Index.php @@ -0,0 +1,894 @@ + 'Ostermontag', + ]; + + protected $queryString = [ + 'view' => ['except' => 'month'], + 'date' => ['except' => ''], + 'search' => ['except' => ''], + 'from' => ['except' => ''], + 'to' => ['except' => ''], + ]; + + protected $listeners = ['eventCreated' => 'loadEvents']; + + public function mount() + { + $tz = auth()->user()->timezone; + + $this->date = $this->date + ? Carbon::parse($this->date, $tz) + : now($tz); + + $this->loadEvents(); + } + + public function updatedView() + { + $this->loadEvents(); + } + + public function setView($view) + { + $this->view = $view; + $this->loadEvents(); + } + + public function loadEvents() + { + $tz = auth()->user()->timezone; + $user = auth()->user(); + + // πŸ”₯ VIEW RANGE + if ($this->view === 'month') { + $start = $this->date->copy()->startOfMonth()->startOfWeek(); + $end = $this->date->copy()->endOfMonth()->endOfWeek(); + } elseif ($this->view === 'week') { + $start = $this->date->copy()->startOfWeek(); + $end = $this->date->copy()->endOfWeek(); + } else { + $start = $this->date->copy()->startOfDay(); + $end = $this->date->copy()->endOfDay(); + } + + $query = $user->events(); + + // πŸ”₯ SEARCH + if ($this->search) { + $query->where('title', 'like', '%' . $this->search . '%'); + } + + // πŸ”₯ FILTER (richtig fΓΌr Multi-Day!) + if ($this->from) { + $from = Carbon::parse($this->from)->startOfDay(); + + $query->where(function ($q) use ($from) { + $q->where('starts_at', '>=', $from) + ->orWhere('ends_at', '>=', $from); + }); + } + + if ($this->to) { + $to = Carbon::parse($this->to)->endOfDay(); + + $query->where(function ($q) use ($to) { + $q->where('starts_at', '<=', $to) + ->orWhere('ends_at', '<=', $to); + }); + } + + // πŸ”₯ RANGE (WICHTIG) + $rawEvents = $query->where(function ($q) use ($start, $end) { + $q->whereBetween('starts_at', [$start, $end]) + ->orWhereBetween('ends_at', [$start, $end]) + ->orWhere(function ($q2) use ($start, $end) { + $q2->where('starts_at', '<=', $start) + ->where('ends_at', '>=', $end); + }); + })->get(); + + $this->rawEvents = $rawEvents; + // πŸ”₯ SPLIT EVENTS IN DAYS + $events = []; + + foreach ($rawEvents as $event) { + + $startDate = $event->starts_at + ->copy() + ->setTimezone($tz) + ->startOfDay(); + + $endDate = $event->ends_at + ? $event->ends_at->copy()->setTimezone($tz)->startOfDay() + : $startDate; + + for ($date = $startDate->copy(); $date <= $endDate; $date->addDay()) { + $events[$date->format('Y-m-d')][] = $event; + } + } + + $this->events = collect($events)->map(fn($g) => collect($g)); + } + + public function updatedSearch() + { + $this->loadEvents(); + } + + public function updatedFrom() + { + $this->loadEvents(); + } + + public function updatedTo() + { + $this->loadEvents(); + } + + public function updateEventTime($eventId, $start, $end) + { + $event = \App\Models\Event::findOrFail($eventId); + + $tz = auth()->user()->timezone ?? 'UTC'; + + $event->update([ + 'starts_at' => Carbon::parse($start, $tz)->utc(), + 'ends_at' => $end ? Carbon::parse($end, $tz)->utc() : null, + ]); + + $this->loadEvents(); + } + + public function moveEventToDate($eventId, $date) + { + $event = \App\Models\Event::findOrFail($eventId); + + $tz = auth()->user()->timezone ?? 'UTC'; + + $start = $event->starts_at->copy()->timezone($tz); + $end = $event->ends_at?->copy()->timezone($tz); + + /* + |-------------------------------------------------------------------------- + | πŸ”₯ FALL 1: ALL DAY ODER MULTI DAY + |-------------------------------------------------------------------------- + */ + $isMultiDay = $end && $start->toDateString() !== $end->toDateString(); + + if ($event->is_all_day || $isMultiDay) { + + // πŸ‘‰ Tage-Differenz behalten (NICHT Sekunden!) + $daySpan = $end + ? $start->startOfDay()->diffInDays($end->startOfDay()) + : 0; + + // πŸ‘‰ neuer Start (immer 00:00 bei allday) + $newStart = Carbon::parse($date, $tz)->startOfDay(); + + // πŸ‘‰ neuer End + $newEnd = $end + ? $newStart->copy()->addDays($daySpan)->endOfDay() + : null; + + } else { + + /* + |-------------------------------------------------------------------------- + | πŸ”₯ FALL 2: NORMALER TERMIN + |-------------------------------------------------------------------------- + */ + $duration = $end + ? $start->diffInSeconds($end) + : 3600; + + $newStart = Carbon::parse( + $date . ' ' . $start->format('H:i'), + $tz + ); + + $newEnd = $end + ? $newStart->copy()->addSeconds($duration) + : null; + } + + /* + |-------------------------------------------------------------------------- + | SAVE (UTC!) + |-------------------------------------------------------------------------- + */ + $event->update([ + 'starts_at' => $newStart->utc(), + 'ends_at' => $newEnd?->utc(), + ]); + + $this->loadEvents(); + } + +// public function moveEventToDate($eventId, $date) +// { +// $event = \App\Models\Event::findOrFail($eventId); +// +// $tz = auth()->user()->timezone ?? 'UTC'; +// +// // aktuelle Zeiten holen (in User TZ) +// $start = $event->starts_at->copy()->timezone($tz); +// $end = $event->ends_at?->copy()->timezone($tz); +// +// // πŸ”₯ Dauer berechnen (wichtig fΓΌr Multi-Day) +// $duration = $end +// ? $start->diffInSeconds($end) +// : 3600; +// +// // πŸ”₯ neues Datum + gleiche Uhrzeit +// $newStart = \Carbon\Carbon::parse( +// $date . ' ' . $start->format('H:i'), +// $tz +// )->utc(); +// +// $newEnd = $end +// ? $newStart->copy()->addSeconds($duration) +// : null; +// +// $event->update([ +// 'starts_at' => $newStart, +// 'ends_at' => $newEnd, +// ]); +// +// $this->loadEvents(); +// } + + public function goToToday() + { + $this->date = now(auth()->user()->timezone); + $this->loadEvents(); + } + + public function next() + { + if ($this->view === 'month') $this->date->addMonth(); + if ($this->view === 'week') $this->date->addWeek(); + if ($this->view === 'day') $this->date->addDay(); + + $this->loadEvents(); + } + + public function prev() + { + if ($this->view === 'month') $this->date->subMonth(); + if ($this->view === 'week') $this->date->subWeek(); + if ($this->view === 'day') $this->date->subDay(); + + $this->loadEvents(); + } + + public function moveEventToDateTime($eventId, $date, $time) + { + $event = Event::findOrFail($eventId); + + $tz = auth()->user()->timezone ?? config('app.timezone'); + + if (!preg_match('/^\d{2}:\d{2}$/', $time)) { + return; + } + + [$hour, $minute] = explode(':', $time); + + $hour = min(max((int)$hour, 0), 23); + $minute = min(max((int)$minute, 0), 59); + + // πŸ”₯ WICHTIG: alte Zeiten in USER TZ holen + $oldStart = $event->starts_at->copy()->setTimezone($tz); + $oldEnd = $event->ends_at?->copy()->setTimezone($tz); + + $duration = $oldEnd + ? $oldStart->diffInMinutes($oldEnd) + : 60; + + if ($duration <= 0) { + $duration = 60; + } + + // πŸ”₯ neue Zeit in USER TZ setzen + $newStart = Carbon::parse($date, $tz)->setTime($hour, $minute); + $newEnd = $newStart->copy()->addMinutes($duration); + + // πŸ”₯ zurΓΌck in UTC speichern + $event->starts_at = $newStart->copy()->utc(); + $event->ends_at = $newEnd->copy()->utc(); + + $event->save(); + + $this->loadEvents(); + } + + + public function updateEventTimeAndDate($eventId, $date, $start, $end) + { + + $event = Event::findOrFail($eventId); + + $tz = auth()->user()->timezone ?? config('app.timezone'); + + $start = Carbon::parse($date.' '.$start, $tz)->utc(); + $end = Carbon::parse($date.' '.$end, $tz)->utc(); + + if ($end->lessThanOrEqualTo($start)) { + $end = $start->copy()->addHour(); + } + + $event->update([ + 'starts_at' => $start, + 'ends_at' => $end, + ]); + + $this->loadEvents(); + } + + + public function updateEventRange($id, $startDate, $endDate, $startTime, $endTime) + { + $event = Event::findOrFail($id); + + $tz = auth()->user()->timezone; + + $start = Carbon::parse("$startDate $startTime", $tz)->utc(); + $end = Carbon::parse("$endDate $endTime", $tz)->utc(); + + if ($end->lessThanOrEqualTo($start)) { + $end = $start->copy()->addHour(); + } + + $event->update([ + 'starts_at' => $start, + 'ends_at' => $end, + ]); + + $this->loadEvents(); + } + + + private function transformAllDayEvents($events) + { + return $events->map(function ($event) { + + $start = $event->starts_at->copy(); + $end = $event->ends_at?->copy() ?? $start; + + $startDay = $start->startOfDay(); + $endDay = $end->startOfDay(); + + return [ + 'id' => $event->id, + 'title' => $event->title, + 'color' => $event->color, + + 'start_day' => $startDay, + 'end_day' => $endDay, + + 'duration_days' => $startDay->diffInDays($endDay) + 1, + ]; + }); + } + + private function mapAllDayToWeek($events, $weekStart) + { + return $events->map(function ($event) use ($weekStart) { + + $startOffset = $event['start_day']->diffInDays($weekStart, false); + $endOffset = $event['end_day']->diffInDays($weekStart, false); + + // clamp auf Woche + $start = max($startOffset, 0); + $end = min($endOffset, 6); + + $span = ($end - $start) + 1; + + return [ + ...$event, + 'left' => ($start / 7) * 100, + 'width' => ($span / 7) * 100, + ]; + }); + } + + public function getCalendarDaysProperty(): \Illuminate\Support\Collection + { + $tz = auth()->user()->timezone; + + return collect(range(0, 6))->map(function ($i) use ($tz) { + + $day = $this->date->copy()->startOfWeek()->addDays($i); + $key = $day->format('Y-m-d'); + + $events = collect($this->events[$key] ?? []); + + /* + |-------------------------------------------------------------------------- + | πŸ”₯ ALLDAY + MULTIDAY RAUSFILTERN (WICHTIG!) + |-------------------------------------------------------------------------- + */ + $filtered = $events->reject(function ($event) use ($tz) { + + $start = $event->starts_at->copy()->setTimezone($tz); + $end = $event->ends_at?->copy()->setTimezone($tz) ?? $start; + + // πŸ‘‰ gleiche Logik wie in getAllDayEventsProperty + $isMultiDay = $end->startOfDay()->gt($start->startOfDay()); + + return $event->is_all_day || $isMultiDay; + }); + + /* + |-------------------------------------------------------------------------- + | πŸ”₯ TIMED EVENTS TRANSFORMIEREN + |-------------------------------------------------------------------------- + */ + $mapped = $filtered + ->map(fn($event) => $this->transformEvent($event, $day, $tz)) + ->sortBy('start') + ->values(); + + /* + |-------------------------------------------------------------------------- + | πŸ”₯ OVERLAP FIX + |-------------------------------------------------------------------------- + */ + $timed = $mapped->map(function ($event) use ($mapped) { + + $overlapping = $mapped->filter(function ($e) use ($event) { + return $e['start'] < $event['end'] + && $e['end'] > $event['start']; + })->values(); + + $count = max($overlapping->count(), 1); + + $index = $overlapping->pluck('id')->search($event['id']); + $index = $index === false ? 0 : $index; + + $event['count'] = $count; + $event['index'] = $index; + + return $event; + }); + + return [ + 'date' => $day, + 'key' => $key, + 'timed' => $timed, + ]; + }); + } +// public function getCalendarDaysProperty(): \Illuminate\Support\Collection +// { +// $tz = auth()->user()->timezone; +// +// return collect(range(0, 6))->map(function ($i) use ($tz) { +// +// $day = $this->date->copy()->startOfWeek()->addDays($i); +// $key = $day->format('Y-m-d'); +// +// $events = collect($this->events[$key] ?? []); +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ NUR ALLDAY RAUS +// |-------------------------------------------------------------------------- +// */ +// $mapped = $events +// ->reject(fn($e) => $e->is_all_day) +// ->map(fn($event) => $this->transformEvent($event, $day, $tz)) +// ->sortBy('start') +// ->values(); +// +// /* +// |-------------------------------------------------------------------------- +// | OVERLAP +// |-------------------------------------------------------------------------- +// */ +// $timed = $mapped->map(function ($event) use ($mapped) { +// +// $overlapping = $mapped->filter(function ($e) use ($event) { +// return $e['start'] < $event['end'] +// && $e['end'] > $event['start']; +// })->values(); +// +// $count = max($overlapping->count(), 1); +// +// $index = $overlapping->pluck('id')->search($event['id']); +// $index = $index === false ? 0 : $index; +// +// $event['count'] = $count; +// $event['index'] = $index; +// +// return $event; +// }); +// +// return [ +// 'date' => $day, +// 'key' => $key, +// 'timed' => $timed, +// ]; +// }); +// } + + // public function getCalendarDaysProperty(): \Illuminate\Support\Collection +// { +// $tz = auth()->user()->timezone; +// +// return collect(range(0, 6))->map(function ($i) use ($tz) { +// +// $day = $this->date->copy()->startOfWeek()->addDays($i); +// $key = $day->format('Y-m-d'); +// +// $events = collect($this->events[$key] ?? []); +// +// // πŸ‘‰ AllDay Events +// $allDay = $events->filter(fn($e) => $e->is_all_day); +// +// // πŸ‘‰ Timed Events transformieren +// $mapped = $events +// ->reject(fn($e) => $e->is_all_day) +// ->map(fn($event) => $this->transformEvent($event, $day, $tz)) +// ->sortBy('start') // πŸ”₯ wichtig fΓΌr stabile Reihenfolge +// ->values(); +// +// // πŸ‘‰ Overlap berechnen +// $timed = $mapped->map(function ($event) use ($mapped) { +// +// $overlapping = $mapped->filter(function ($e) use ($event) { +// return $e['start'] < $event['end'] +// && $e['end'] > $event['start']; +// })->values(); +// +// $count = max($overlapping->count(), 1); +// +// $index = $overlapping->pluck('id')->search($event['id']); +// $index = $index === false ? 0 : $index; +// +// $event['count'] = $count; +// $event['index'] = $index; +// +// return $event; +// }); +// +// return [ +// 'date' => $day, +// 'key' => $key, +// 'allDay' => $allDay, +// 'timed' => $timed, +// ]; +// }); +// } + + +// public function getAllDayEventsProperty() +// { +// $tz = auth()->user()->timezone; +// +// $weekStart = $this->date->copy()->startOfWeek(); +// $weekEnd = $this->date->copy()->endOfWeek(); +// +// return collect($this->rawEvents) +// ->filter(fn($event) => $event->is_all_day == true) +// ->map(function ($event) use ($weekStart, $weekEnd, $tz) { +// +// $start = $event->starts_at->copy()->setTimezone($tz); +// $end = $event->ends_at +// ? $event->ends_at->copy()->setTimezone($tz) +// : $start; +// +// if ($end->lt($weekStart) || $start->gt($weekEnd)) { +// return null; +// } +// +// $startClamped = $start->copy()->max($weekStart)->startOfDay(); +// $endClamped = $end->copy()->min($weekEnd)->startOfDay(); +// +// $startOffset = $weekStart->diffInDays($startClamped); +// $endOffset = $weekStart->diffInDays($endClamped); +// +// $span = max(($endOffset - $startOffset) + 1, 1); +// +// return [ +// 'id' => $event->id, +// 'title' => $event->title, +// 'color' => $event->color, +// +// // πŸ”₯ DAS HAT GEFEHLT +// 'start' => $startClamped, +// 'end' => $endClamped, +// +// 'left' => ($startOffset / 7) * 100, +// 'width' => ($span / 7) * 100, +// ]; +// }) +// ->filter() +// ->values(); +// } + +// public function getAllDayEventsProperty() +// { +// $tz = auth()->user()->timezone; +// +// $weekStart = $this->date->copy()->startOfWeek()->startOfDay(); +// $weekEnd = $this->date->copy()->endOfWeek()->endOfDay(); +// +// $events = collect($this->rawEvents) +// +// ->filter(function ($event) use ($tz) { +// +// $start = $event->starts_at->copy()->setTimezone($tz); +// $end = $event->ends_at?->copy()->setTimezone($tz) ?? $start; +// +// // πŸ‘‰ echtes AllDay oder MultiDay +// return $event->is_all_day +// || $end->startOfDay()->gt($start->startOfDay()); +// }) +// +// ->map(function ($event) use ($tz, $weekStart, $weekEnd) { +// +// $start = $event->starts_at->copy()->setTimezone($tz); +// $end = $event->ends_at +// ? $event->ends_at->copy()->setTimezone($tz) +// : $start; +// +// /* +// |-------------------------------------------------------------------------- +// | ❌ außerhalb β†’ raus +// |-------------------------------------------------------------------------- +// */ +// if ($end->lt($weekStart) || $start->gt($weekEnd)) { +// return null; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ CLAMP AUF WOCHE +// |-------------------------------------------------------------------------- +// */ +// $startClamped = $start->lt($weekStart) +// ? $weekStart->copy() +// : $start->copy(); +// +// $endClamped = $end->gt($weekEnd) +// ? $weekEnd->copy() +// : $end->copy(); +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ WICHTIG: START/END auf DAY LEVEL +// |-------------------------------------------------------------------------- +// */ +// $startDay = $startClamped->copy()->startOfDay(); +// $endDay = $endClamped->copy()->startOfDay(); +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ OFFSET BERECHNUNG (STABIL) +// |-------------------------------------------------------------------------- +// */ +// $startOffset = $weekStart->diffInDays($startDay); +// $endOffset = $weekStart->diffInDays($endDay); +// +// $span = max(1, ($endOffset - $startOffset) + 1); +// +// return [ +// 'id' => $event->id, +// 'title' => $event->title, +// 'color' => $event->color, +// +// 'start' => $startDay, +// 'end' => $endDay, +// +// 'left' => ($startOffset / 7) * 100, +// 'width' => ($span / 7) * 100, +// ]; +// }) +// +// ->filter() +// ->sortBy('start') +// ->values(); +// +// return $this->stackAllDayEvents($events); +// } + + public function getAllDayEventsProperty() + { + $tz = auth()->user()->timezone; + + $weekStart = $this->date->copy()->startOfWeek()->startOfDay(); + $weekEnd = $this->date->copy()->endOfWeek()->endOfDay(); + + $events = collect($this->rawEvents) + + ->filter(function ($event) use ($tz) { + + $start = $event->starts_at->copy()->setTimezone($tz); + $end = $event->ends_at?->copy()->setTimezone($tz) ?? $start; + + $isMultiDay = $end->startOfDay()->gt($start->startOfDay()); + + return $event->is_all_day || $isMultiDay; + }) + + ->map(function ($event) use ($tz, $weekStart, $weekEnd) { + + $start = $event->starts_at->copy()->setTimezone($tz)->startOfDay(); + $end = $event->ends_at + ? $event->ends_at->copy()->setTimezone($tz)->startOfDay() + : $start; + + if ($end->lt($weekStart) || $start->gt($weekEnd)) { + return null; + } + + $startClamped = $start->lt($weekStart) ? $weekStart->copy() : $start; + $endClamped = $end->gt($weekEnd) ? $weekEnd->copy() : $end; + + $startOffset = $weekStart->diffInDays($startClamped); + $endOffset = $weekStart->diffInDays($endClamped); + + $span = ($endOffset - $startOffset) + 1; + + return [ + 'id' => $event->id, + 'title' => $event->title, + 'color' => $event->color, + + 'start' => $startClamped, + 'end' => $endClamped, + + 'col' => $startOffset, + 'span' => $span, + ]; + }) + + ->filter() + ->sortBy('start') + ->values(); + + return $this->stackAllDayEvents($events); + } + + private function stackAllDayEvents($events) + { + $rows = []; + + foreach ($events as $event) { + + $placed = false; + + foreach ($rows as &$row) { + + $collision = collect($row)->first(function ($e) use ($event) { + + $aStart = $event['col']; + $aEnd = $event['col'] + $event['span'] - 1; + + $bStart = $e['col']; + $bEnd = $e['col'] + $e['span'] - 1; + + return !($aEnd < $bStart || $aStart > $bEnd); + }); + + if (!$collision) { + $row[] = $event; + $placed = true; + break; + } + } + + if (!$placed) { + $rows[] = [$event]; + } + } + + return collect($rows)->map(function ($row, $rowIndex) { + + return collect($row)->map(function ($event) use ($rowIndex) { + return [ + ...$event, + 'row' => $rowIndex + ]; + }); + + })->flatten(1); + } + + private function transformEvent($event, $day, $tz) + { + $start = $event->starts_at->copy()->setTimezone($tz); + $end = $event->ends_at?->copy()->setTimezone($tz); + + $dayStart = $day->copy()->startOfDay(); + $dayEnd = $day->copy()->endOfDay(); + + // πŸ‘‰ Seminar / MultiDay Fix + if ($end && $start->diffInDays($end) > 0 && !$event->is_all_day) { + + $effectiveStart = $dayStart->copy()->setTime($start->hour, $start->minute); + $effectiveEnd = $dayStart->copy()->setTime($end->hour, $end->minute); + + // πŸ‘‰ Exceptions + $exceptions = is_array($event->exceptions) + ? $event->exceptions + : json_decode($event->exceptions ?? '[]', true); + + $exception = collect($exceptions) + ->firstWhere('date', $day->format('Y-m-d')); + + if ($exception) { + if (!empty($exception['start'])) { + [$h, $m] = explode(':', $exception['start']); + $effectiveStart->setTime($h, $m); + } + + if (!empty($exception['end'])) { + [$h, $m] = explode(':', $exception['end']); + $effectiveEnd->setTime($h, $m); + } + } + + } else { + $effectiveStart = $start; + $effectiveEnd = $end ?? $start->copy()->addHour(); + } + + $hourHeight = 64; + + $top = ($effectiveStart->hour * $hourHeight) + + (($effectiveStart->minute / 60) * $hourHeight); + + $height = max($effectiveStart->diffInMinutes($effectiveEnd), 1) / 60 * $hourHeight; + + + return [ + 'model' => $event, + 'id' => $event->id, + 'title' => $event->title, + 'top' => $top, + 'height' => $height, + 'start' => $effectiveStart, + 'end' => $effectiveEnd, + ]; + } + + public function colorToRgba($color, $opacity = 1): string + { + if (!$color) return 'rgba(99,102,241,0.8)'; // fallback + + $color = ltrim($color, '#'); + + $r = hexdec(substr($color, 0, 2)); + $g = hexdec(substr($color, 2, 2)); + $b = hexdec(substr($color, 4, 2)); + + return "rgba($r, $g, $b, $opacity)"; + } + + + public function render() + { + return view('livewire.calendar.index') + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Calendar/Modals/DayEventsModal.php b/src/app/Livewire/Calendar/Modals/DayEventsModal.php new file mode 100644 index 0000000..d68b331 --- /dev/null +++ b/src/app/Livewire/Calendar/Modals/DayEventsModal.php @@ -0,0 +1,29 @@ +date = $date; + + $user = auth()->user(); + + $this->events = $user->events() + ->whereDate('starts_at', $date) + ->orderBy('starts_at') + ->get(); + } + + public function render() + { + return view('livewire.calendar.modals.day-events-modal'); + } +} diff --git a/src/app/Livewire/Calendar/Modals/EventModal.php b/src/app/Livewire/Calendar/Modals/EventModal.php new file mode 100644 index 0000000..818ebe0 --- /dev/null +++ b/src/app/Livewire/Calendar/Modals/EventModal.php @@ -0,0 +1,823 @@ +user()->timezone; + + $this->eventId = $eventId; + + if ($eventId) { + + $event = Event::findOrFail($eventId); + + $this->title = $event->title; + $this->color = $event->color ?? '#6366f1'; + + $start = $event->starts_at->timezone($tz); + $end = $event->ends_at?->timezone($tz); + + $this->start_date = $start->format('Y-m-d'); + $this->end_date = $end ? $end->format('Y-m-d') : $this->start_date; + + $this->date = $date ?? $this->start_date; + + // πŸ”₯ GLOBAL ZEIT + $this->time = $start->format('H:i'); + $this->end_time = $end?->format('H:i'); + + // πŸ”₯ DEFAULT β†’ exception = global + $this->exception_time = $this->time; + $this->exception_end_time = $this->end_time; + + // πŸ”₯ EXCEPTIONS CHECK + $exceptions = $event->exceptions ?? []; + + if (is_string($exceptions)) { + $exceptions = json_decode($exceptions, true); + } + + if (is_array($exceptions)) { + foreach ($exceptions as $ex) { + + if (($ex['date'] ?? null) === $this->date) { + + $this->use_exception = true; + + $this->exception_time = $ex['start'] ?? $this->time; + $this->exception_end_time = $ex['end'] ?? $this->end_time; + } + } + } + + $this->is_all_day = $event->is_all_day ?? false; + + if ($this->is_all_day) { + $this->time = null; + $this->end_time = null; + $this->exception_time = null; + $this->exception_end_time = null; + } + + } else { + + $this->date = $date ?? now($tz)->format('Y-m-d'); + $this->start_date = $this->date; + $this->end_date = $this->date; + } + } + + public function updatedUseException($value) + { + if ($value) { + $this->exception_time = $this->time; + $this->exception_end_time = $this->end_time; + } + } + + public function save() + { + if (!trim($this->title)) return; + + $event = Event::findOrFail($this->eventId); + $tz = auth()->user()->timezone; + + // πŸ”₯ EXCEPTIONS + $exceptions = $event->exceptions ?? []; + + if (is_string($exceptions)) { + $exceptions = json_decode($exceptions, true); + } + + if (!is_array($exceptions)) { + $exceptions = []; + } + + $exceptions = collect($exceptions) + ->filter(fn($ex) => + isset($ex['date']) && + $ex['date'] >= $this->start_date && + $ex['date'] <= $this->end_date + ) + ->values() + ->toArray(); + + // alte entfernen + $exceptions = collect($exceptions) + ->reject(fn($ex) => $ex['date'] === $this->date) + ->values() + ->toArray(); + + // πŸ”₯ EXCEPTION SETZEN + if ($this->use_exception) { + + $exceptions[] = [ + 'date' => $this->date, + 'start' => $this->exception_time, + 'end' => $this->exception_end_time, + ]; + + $event->update([ + 'exceptions' => $exceptions + ]); + + } else { + + // πŸ”₯ GLOBAL UPDATE + if ($this->is_all_day) { + + $start = Carbon::createFromFormat('Y-m-d', $this->start_date, 'UTC')->startOfDay(); + $end = Carbon::createFromFormat('Y-m-d', $this->end_date ?? $this->start_date, 'UTC')->endOfDay(); + + } else { + + $start = Carbon::parse( + $this->start_date . ' ' . ($this->time ?? '00:00'), + $tz + )->setTimezone('UTC'); + + $end = Carbon::parse( + $this->end_date . ' ' . ($this->end_time ?? $this->time ?? '00:00'), + $tz + )->setTimezone('UTC'); + } + + $event->update([ + 'title' => $this->title, + 'color' => $this->color, + 'starts_at' => $start, + 'ends_at' => $end, + 'is_all_day' => $this->is_all_day, + 'exceptions' => $exceptions, + ]); + } + + $this->dispatch('eventCreated'); + $this->closeModal(); + } + + public function render() + { + return view('livewire.calendar.modals.event-modal'); + } +} + +//class EventModal extends ModalComponent +//{ +// public $eventId = null; +// +// public $date; +// +// public $title = ''; +// public $color = '#6366f1'; +// +// public $time = '12:00'; +// public $end_time = null; +// +// public $exception_time = null; +// public $exception_end_time = null; +// +// public $start_date = null; +// public $end_date = null; +// public $is_all_day = false; +// +// // πŸ”₯ NEU +// public $use_exception = false; +// +// public function mount($date = null, $eventId = null) +// { +// $tz = auth()->user()->timezone; +// +// $this->eventId = $eventId; +// +// if ($eventId) { +// +// $event = Event::findOrFail($eventId); +// +// $this->title = $event->title; +// $this->color = $event->color ?? '#6366f1'; +// +// $start = $event->starts_at->timezone($tz); +// $end = $event->ends_at?->timezone($tz); +// +// $this->start_date = $start->format('Y-m-d'); +// $this->end_date = $end ? $end->format('Y-m-d') : $this->start_date; +// +// $this->date = $date ?? $this->start_date; +// +// $displayStart = $start; +// $displayEnd = $end; +// +// $exceptions = $event->exceptions ?? []; +// +// if (is_string($exceptions)) { +// $exceptions = json_decode($exceptions, true); +// } +// +// // πŸ”₯ CHECK: hat dieser Tag Exception? +// if (is_array($exceptions)) { +// foreach ($exceptions as $ex) { +// +// if (($ex['date'] ?? null) === $this->date) { +// +// $this->use_exception = true; +// +// if (!empty($ex['start'])) { +// $displayStart = $start->copy()->setTimeFromTimeString($ex['start']); +// } +// +// if (!empty($ex['end'])) { +// $displayEnd = $end +// ? $end->copy()->setTimeFromTimeString($ex['end']) +// : $start->copy()->setTimeFromTimeString($ex['end']); +// } +// } +// } +// } +// +// $this->time = $displayStart->format('H:i'); +// $this->end_time = $displayEnd?->format('H:i'); +// +// $this->is_all_day = $event->is_all_day ?? false; +// +// if ($this->is_all_day) { +// $this->time = null; +// $this->end_time = null; +// } +// +// } else { +// +// $this->date = $date ?? now($tz)->format('Y-m-d'); +// $this->start_date = $this->date; +// $this->end_date = $this->date; +// } +// } +// +// public function save() +// { +// if (!trim($this->title)) return; +// +// $event = Event::findOrFail($this->eventId); +// +// $tz = auth()->user()->timezone; +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ EXCEPTIONS laden +// |-------------------------------------------------------------------------- +// */ +// +// $exceptions = $event->exceptions ?? []; +// +// if (is_string($exceptions)) { +// $exceptions = json_decode($exceptions, true); +// } +// +// if (!is_array($exceptions)) { +// $exceptions = []; +// } +// +// // πŸ‘‰ alten Eintrag entfernen +// $exceptions = collect($exceptions) +// ->reject(fn($ex) => $ex['date'] === $this->date) +// ->values() +// ->toArray(); +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ FALL 1: NUR EXCEPTION (kein global update!) +// |-------------------------------------------------------------------------- +// */ +// +// if ($this->use_exception) { +// +// $exceptions[] = [ +// 'date' => $this->date, +// 'start' => $this->time, +// 'end' => $this->end_time, +// ]; +// +// $event->update([ +// 'exceptions' => $exceptions +// ]); +// +// } else { +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ FALL 2: GLOBAL UPDATE +// |-------------------------------------------------------------------------- +// */ +// +// if ($this->is_all_day) { +// +// $start = Carbon::createFromFormat('Y-m-d', $this->start_date, 'UTC')->startOfDay(); +// $end = Carbon::createFromFormat('Y-m-d', $this->end_date ?? $this->start_date, 'UTC')->endOfDay(); +// +// } else { +// +// $start = Carbon::parse( +// $this->start_date . ' ' . ($this->time ?? '00:00'), +// $tz +// )->setTimezone('UTC'); +// +// $end = Carbon::parse( +// $this->end_date . ' ' . ($this->end_time ?? $this->time ?? '00:00'), +// $tz +// )->setTimezone('UTC'); +// } +// +// $event->update([ +// 'title' => $this->title, +// 'color' => $this->color, +// 'starts_at' => $start, +// 'ends_at' => $end, +// 'is_all_day' => $this->is_all_day, +// 'exceptions' => $exceptions, +// ]); +// } +// +// $this->dispatch('eventCreated'); +// $this->closeModal(); +// } +// +// +// public function render() +// { +// return view('livewire.calendar.modals.event-modal'); +// } +//} + +// +// +//namespace App\Livewire\Calendar\Modals; +// +//use App\Models\Event; +//use Carbon\Carbon; +//use LivewireUI\Modal\ModalComponent; +// +//class EventModal extends ModalComponent +//{ +// public $eventId = null; +// +// public $date; +// public $title = ''; +// public $color = '#6366f1'; +// +// public $time = '12:00'; +// public $end_time = null; +// +// public $start_date = null; +// public $end_date = null; +// +// public $is_all_day = false; +// +// public function mount($date = null, $eventId = null) +// { +// $tz = auth()->user()->timezone; +// +// $this->eventId = $eventId; +// +// if ($eventId) { +// +// $event = Event::findOrFail($eventId); +// +// $this->title = $event->title; +// $this->color = $event->color ?? '#6366f1'; +// +// $start = $event->starts_at->timezone($tz); +// $end = $event->ends_at?->timezone($tz); +// +// // πŸ‘‰ GLOBAL (immer korrekt) +// $this->start_date = $start->format('Y-m-d'); +// $this->end_date = $end +// ? $end->format('Y-m-d') +// : $start->format('Y-m-d'); +// +// // πŸ‘‰ aktueller Klick-Tag +// $this->date = $date ?? $this->start_date; +// +// $displayStart = $start; +// $displayEnd = $end; +// +// // πŸ”₯ EXCEPTIONS +// $exceptions = $event->exceptions; +// +// if (is_string($exceptions)) { +// $exceptions = json_decode($exceptions, true); +// } +// +// if (is_array($exceptions) && $this->date) { +// foreach ($exceptions as $ex) { +// +// if (($ex['date'] ?? null) === $this->date) { +// +// if (!empty($ex['start'])) { +// $displayStart = $start->copy()->setTimeFromTimeString($ex['start']); +// } +// +// if (!empty($ex['end'])) { +// $displayEnd = $start->copy()->setTimeFromTimeString($ex['end']); +// } +// } +// } +// } +// +// $this->time = $displayStart->format('H:i'); +// $this->end_time = $displayEnd?->format('H:i'); +// +// $this->is_all_day = $event->is_all_day ?? false; +// +// if ($this->is_all_day) { +// $this->time = null; +// $this->end_time = null; +// } +// +// } else { +// +// $this->date = $date ?? now($tz)->format('Y-m-d'); +// $this->start_date = $this->date; +// } +// } +// +// public function save() +// { +// if (!trim($this->title)) return; +// +// $user = auth()->user(); +// $tz = $user->timezone; +// +// // πŸ”₯ DEFAULT: end_date setzen wenn leer +// if (!$this->end_date) { +// $this->end_date = $this->start_date; +// } +// +// if ($this->is_all_day) { +// +// $start = Carbon::parse($this->start_date)->startOfDay(); +// +// $end = Carbon::parse($this->end_date)->endOfDay(); +// +// } else { +// +// $start = Carbon::parse( +// $this->start_date . ' ' . ($this->time ?? '00:00'), +// $tz +// )->setTimezone('UTC'); +// +// $end = Carbon::parse( +// $this->end_date . ' ' . ($this->end_time ?? $this->time ?? '00:00'), +// $tz +// )->setTimezone('UTC'); +// } +// +// Event::updateOrCreate( +// ['id' => $this->eventId], +// [ +// 'user_id' => $user->id, +// 'title' => $this->title, +// 'starts_at' => $start, +// 'ends_at' => $end, +// 'is_all_day'=> $this->is_all_day, +// 'color' => $this->color, +// ] +// ); +// +// $this->dispatch('eventCreated'); +// $this->closeModal(); +// } +// +// +// public function render() +// { +// return view('livewire.calendar.modals.event-modal'); +// } +//} + +//namespace App\Livewire\Calendar\Modals; +// +//use App\Models\Event; +//use Carbon\Carbon; +//use LivewireUI\Modal\ModalComponent; + +//class EventModal extends ModalComponent +//{ +// public $eventId = null; +// +// public $date; +// public $title = ''; +// public $color = '#6366f1'; +// +// public $time = '12:00'; +// +// public $end_date = null; +// public $end_time = null; +// +// public $is_all_day = false; +// +// public function mount($date = null, $eventId = null) +// { +// $tz = auth()->user()->timezone; +// +// $this->eventId = $eventId; +// +// if ($eventId) { +// +// $event = Event::findOrFail($eventId); +// +// $this->title = $event->title; +// +// // πŸ‘‰ Klick-Tag verwenden (WICHTIG) +// $this->date = $date +// ?? $event->starts_at->timezone($tz)->format('Y-m-d'); +// +// $start = $event->starts_at->timezone($tz); +// $end = $event->ends_at?->timezone($tz); +// +// $displayStart = $start; +// $displayEnd = $end; +// +// // πŸ”₯ EXCEPTIONS +// $exceptions = $event->exceptions; +// +// if (is_string($exceptions)) { +// $exceptions = json_decode($exceptions, true); +// } +// +// if (is_array($exceptions) && $this->date) { +// foreach ($exceptions as $ex) { +// +// if (($ex['date'] ?? null) === $this->date) { +// +// if (!empty($ex['start'])) { +// $displayStart = $start->copy()->setTimeFromTimeString($ex['start']); +// } +// +// if (!empty($ex['end'])) { +// $displayEnd = $end +// ? $end->copy()->setTimeFromTimeString($ex['end']) +// : $start->copy()->setTimeFromTimeString($ex['end']); +// } +// } +// } +// } +// +// // πŸ‘‰ Werte setzen +// $this->time = $displayStart->format('H:i'); +// +// if ($displayEnd) { +// +// $eventEndDate = $displayEnd->format('Y-m-d'); +// +// // πŸ‘‰ IMMER echtes Enddatum setzen +// $this->end_date = $eventEndDate; +// +// $this->end_time = $displayEnd->format('H:i'); +// } +// +// // πŸ”₯ ALL DAY LOGIK +// $this->is_all_day = $event->is_all_day ?? false; +// +// // πŸ‘‰ wenn Urlaub β†’ keine Zeit anzeigen +// if ($this->is_all_day) { +// $this->time = null; +// $this->end_time = null; +// } +// +// } else { +// $this->date = $date ?? now($tz)->format('Y-m-d'); +// } +// } +// +// public function save() +// { +// if (!trim($this->title)) return; +// +// $user = auth()->user(); +// $tz = $user->timezone; +// +// // πŸ”₯ FALL 1: GANZTΓ„GIG (Urlaub etc.) +// if ($this->is_all_day) { +// +// $start = Carbon::parse($this->date)->startOfDay(); +// +// $end = $this->end_date +// ? Carbon::parse($this->end_date)->endOfDay() +// : $start->copy()->endOfDay(); +// +// Event::updateOrCreate( +// ['id' => $this->eventId], +// [ +// 'user_id' => $user->id, +// 'title' => $this->title, +// 'starts_at' => $start, +// 'ends_at' => $end, +// 'is_all_day' => true, +// 'color' => $this->color, +// ] +// ); +// +// } else { +// +// // πŸ”₯ NORMAL / SEMINAR +// $start = Carbon::parse( +// $this->date . ' ' . ($this->time ?? '00:00'), +// $tz +// )->setTimezone('UTC'); +// +// $end = null; +// +// if ($this->end_date) { +// +// if ($this->end_time) { +// $end = Carbon::parse( +// $this->end_date . ' ' . $this->end_time, +// $tz +// )->setTimezone('UTC'); +// } else { +// $end = Carbon::parse($this->end_date)->endOfDay(); +// } +// } +// +// Event::updateOrCreate( +// ['id' => $this->eventId], +// [ +// 'user_id' => $user->id, +// 'title' => $this->title, +// 'starts_at' => $start, +// 'ends_at' => $end, +// 'is_all_day' => false, +// ] +// ); +// } +// +// $this->dispatch('eventCreated'); +// $this->closeModal(); +// } +// +// public function render() +// { +// return view('livewire.calendar.modals.event-modal'); +// } +//} + +// +// +//namespace App\Livewire\Calendar\Modals; +// +//use App\Models\Event; +//use Carbon\Carbon; +//use LivewireUI\Modal\ModalComponent; +// +//class EventModal extends ModalComponent +//{ +// public $eventId = null; +// +// public $date; +// public $title = ''; +// +// public $time = '12:00'; +// +// public $end_date = null; +// public $end_time = null; +// public $is_all_day = false; +// +// public function mount($date = null, $eventId = null) +// { +// $tz = auth()->user()->timezone; +// +// $this->eventId = $eventId; +// +// if ($eventId) { +// +// $event = Event::findOrFail($eventId); +// +// $this->title = $event->title; +// +// // βœ… WICHTIG: geklickten Tag verwenden +// $this->date = $date +// ?? $event->starts_at->timezone($tz)->format('Y-m-d'); +// +// $this->time = $event->starts_at->timezone($tz)->format('H:i'); +// +// $start = $event->starts_at->timezone($tz); +// $end = $event->ends_at?->timezone($tz); +// +// $displayStart = $start; +// $displayEnd = $end; +// +//// πŸ”₯ EXCEPTIONS berΓΌcksichtigen +// $exceptions = $event->exceptions; +// +// if (is_string($exceptions)) { +// $exceptions = json_decode($exceptions, true); +// } +// +// if (is_array($exceptions) && $date) { +// +// foreach ($exceptions as $ex) { +// +// if (($ex['date'] ?? null) === $date) { +// +// if (!empty($ex['start'])) { +// $displayStart = $start->copy()->setTimeFromTimeString($ex['start']); +// } +// +// if (!empty($ex['end'])) { +// $displayEnd = $start->copy()->setTimeFromTimeString($ex['end']); +// } +// } +// } +// } +// +//// πŸ”₯ Werte ins Modal ΓΌbernehmen +// $this->time = $displayStart->format('H:i'); +// +// if ($displayEnd) { +// $this->end_time = $displayEnd->format('H:i'); +// $this->end_date = $displayEnd->format('Y-m-d'); +// } +// +// } else { +// $this->date = $date ?? now($tz)->format('Y-m-d'); +// } +// } +// +// public function save() +// { +// if (!trim($this->title)) return; +// +// $user = auth()->user(); +// $tz = $user->timezone; +// +// $start = Carbon::parse($this->date . ' ' . $this->time, $tz)->setTimezone('UTC'); +// +// $end = null; +// +// // πŸ”₯ END DATUM gesetzt +// if ($this->end_date) { +// +// // πŸ‘‰ FALL 1: mit Uhrzeit (Seminar) +// if ($this->end_time) { +// +// $end = Carbon::parse( +// $this->end_date . ' ' . $this->end_time, +// $tz +// )->setTimezone('UTC'); +// +// } // πŸ‘‰ FALL 2: ohne Uhrzeit (Urlaub) +// else { +// +// $end = Carbon::parse($this->end_date)->endOfDay(); +// } +// } +// +// Event::updateOrCreate( +// ['id' => $this->eventId], +// [ +// 'user_id' => $user->id, +// 'title' => $this->title, +// 'starts_at' => $start, +// 'ends_at' => $end, +// 'is_all_day' => !$this->end_time && $this->end_date, +// ] +// ); +// +// $this->dispatch('eventCreated'); +// $this->closeModal(); +// } +// +// public function render() +// { +// return view('livewire.calendar.modals.event-modal'); +// } +//} diff --git a/src/app/Livewire/Calendar/Sidebar.php b/src/app/Livewire/Calendar/Sidebar.php new file mode 100644 index 0000000..b3825b6 --- /dev/null +++ b/src/app/Livewire/Calendar/Sidebar.php @@ -0,0 +1,64 @@ + 'openEvent', + 'sidebar:createEvent' => 'createEvent', + 'sidebar:close' => 'close', + + 'event:save' => 'saveEvent', + 'event:delete' => 'deleteEvent', + ]; + + public function saveEvent() + { + $this->dispatch('eventForm:save'); + } + + public function deleteEvent() + { + $this->dispatch('eventForm:delete'); + } + + public function openEvent($eventId, $date = null) + { + $this->eventId = $eventId; + $this->date = $date; // πŸ”₯ WICHTIG + $this->mode = 'edit'; + $this->open = true; + } + + public function createEvent($date = null, $time = null) + { + $this->eventId = null; + $this->date = $date; + $this->time = $time; + $this->mode = 'create'; + $this->open = true; + } + + public function close() + { + $this->reset(['eventId', 'date', 'time']); + $this->open = false; + } + + public function render() + { + return view('livewire.calendar.sidebar'); + } + +} diff --git a/src/app/Livewire/Calendar/WeekCanvas.php b/src/app/Livewire/Calendar/WeekCanvas.php new file mode 100644 index 0000000..bbc13c9 --- /dev/null +++ b/src/app/Livewire/Calendar/WeekCanvas.php @@ -0,0 +1,662 @@ +id(); + + return [ + 'eventCreated' => '$refresh', + "echo-private:calendar.{$userId},.calendar.updated" => '$refresh', + ]; + } + + protected $queryString = [ + 'view' => ['except' => 'week'], // ?view= nur wenn NICHT week + 'currentDate' => ['except' => '', 'as' => 'date'], // als ?date=YYYY-MM-DD in der URL + ]; + + public function mount(): void + { + // Livewire fΓΌllt $currentDate aus ?date= bereits vor mount() – + // nur setzen wenn noch leer (kein URL-Parameter vorhanden) + if (empty($this->currentDate)) { + $this->currentDate = now(auth()->user()->timezone)->toDateString(); + } + } + + public function navigate(string $direction): void + { + $date = Carbon::parse($this->currentDate, auth()->user()->timezone); + + match ($this->view) { + 'week' => $direction === 'prev' ? $date->subWeek() : $date->addWeek(), + 'month' => $direction === 'prev' ? $date->subMonth() : $date->addMonth(), + 'day' => $direction === 'prev' ? $date->subDay() : $date->addDay(), + }; + + $this->currentDate = $date->toDateString(); + } + + public function getTitleStringProperty(): string + { + $userTz = auth()->user()->timezone; + $date = Carbon::parse($this->currentDate, $userTz); + $months = ['Januar', 'Februar', 'MΓ€rz', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; + + return match ($this->view) { + 'month' => $months[$date->month - 1] . ' ' . $date->year, + 'week' => $this->weekTitleString($date, $months), + 'day' => $date->isoFormat('dddd, D. MMMM YYYY'), + }; + } + + private function weekTitleString(Carbon $date, array $months): string + { + $start = $date->copy()->startOfWeek(Carbon::MONDAY); + $end = $date->copy()->endOfWeek(Carbon::SUNDAY); + + if ($start->month === $end->month) { + return $months[$start->month - 1] . ' ' . $start->year; + } + + return $months[$start->month - 1] . ' – ' . $months[$end->month - 1] . ' ' . $end->year; + } + + public function getWeekDaysProperty(): array + { + $userTz = auth()->user()->timezone; + $start = Carbon::parse($this->currentDate, $userTz)->startOfWeek(Carbon::MONDAY); + + return collect(range(0, 6)) + ->map(fn($i) => $start->copy()->addDays($i)) + ->all(); + } + + public function rangeStart(): Carbon + { + $date = Carbon::parse($this->currentDate, auth()->user()->timezone); + + return match ($this->view) { + 'month' => $date->copy()->startOfMonth()->startOfWeek(Carbon::MONDAY)->startOfDay()->utc(), + 'week' => $date->copy()->startOfWeek(Carbon::MONDAY)->startOfDay()->utc(), + 'day' => $date->copy()->startOfDay()->utc(), + }; + } + + public function rangeEnd(): Carbon + { + $date = Carbon::parse($this->currentDate, auth()->user()->timezone); + + return match ($this->view) { + 'month' => $date->copy()->endOfMonth()->endOfWeek(Carbon::SUNDAY)->endOfDay()->utc(), + 'week' => $date->copy()->endOfWeek(Carbon::SUNDAY)->endOfDay()->utc(), + 'day' => $date->copy()->endOfDay()->utc(), + }; + } + + /** + * Verschiebt einen Termin auf ein neues Datum/Uhrzeit. + * + * @param string|null $grabDate Das Datum der Kopie, die der User gegriffen hat. + * Wenn angegeben, wird der gesamte Termin relativ + * verschoben (dayDelta = dropDate – grabDate), statt + * den Start absolut auf $newDate zu setzen. + * NΓΆtig fΓΌr mehrtΓ€gige Events, die auf mehreren Tagen + * gleichzeitig angezeigt werden. + */ + public function moveEvent(string $eventId, string $newDate, int $newHour, int $newMinute = 0, ?string $grabDate = null): void + { + $event = Event::where('user_id', auth()->id())->findOrFail($eventId); + $userTz = auth()->user()->timezone; + + if ($grabDate !== null) { + // Relatives Verschieben: Drop-Tag – Grab-Tag = dayDelta + $grabDT = Carbon::parse($grabDate, $userTz)->startOfDay(); + $dropDT = Carbon::parse($newDate, $userTz)->startOfDay(); + $dayDelta = (int) $grabDT->diffInDays($dropDT, false); // signed + + $eventStartLocal = $event->starts_at->copy()->setTimezone($userTz)->addDays($dayDelta); + $newStart = $eventStartLocal->setHour($newHour)->setMinute($newMinute)->setSecond(0)->utc(); + } else { + $newStart = Carbon::parse($newDate, $userTz) + ->setHour($newHour)->setMinute($newMinute)->setSecond(0)->utc(); + } + + $duration = $event->ends_at + ? $event->starts_at->diffInMinutes($event->ends_at) + : 60; + + $event->starts_at = $newStart; + $event->ends_at = $newStart->copy()->addMinutes($duration); + $event->start_date = $newStart->copy()->setTimezone($userTz)->toDateString(); + $event->end_date = $newStart->copy()->addMinutes($duration)->setTimezone($userTz)->toDateString(); + $event->save(); + } + + public function moveAllDayEvent(string $eventId, int $dayDelta): void + { + if ($dayDelta === 0) return; + + $event = Event::where('user_id', auth()->id())->findOrFail($eventId); + $userTz = auth()->user()->timezone; + + // Originale Timestamps in User-TZ (Uhrzeit bleibt erhalten) + $startLocal = $event->starts_at->copy()->setTimezone($userTz); + $endLocal = $event->ends_at + ? $event->ends_at->copy()->setTimezone($userTz) + : $startLocal->copy(); + + // Beide Timestamps um exakt $dayDelta Tage verschieben (Uhrzeit bleibt) + $newStartLocal = $startLocal->copy()->addDays($dayDelta); + $newEndLocal = $endLocal->copy()->addDays($dayDelta); + + // Datum-Spalten (lesbar) und Timestamp-Spalten (UTC) synchron schreiben + $event->start_date = $newStartLocal->toDateString(); + $event->end_date = $newEndLocal->toDateString(); + $event->starts_at = $newStartLocal->copy()->utc(); + $event->ends_at = $newEndLocal->copy()->utc(); + $event->save(); + } + + public function openSidebar(string $eventId, string $date): void + { + $this->dispatch('sidebar:openEvent', eventId: $eventId, date: $date); + } + + public function createEventAt(string $date, int $hour = 9, int $minute = 0): void + { + $time = str_pad($hour, 2, '0', STR_PAD_LEFT) . ':' . str_pad($minute, 2, '0', STR_PAD_LEFT); + $this->dispatch('sidebar:createEvent', date: $date, time: $time); + } + + public function resizeEvent(string $eventId, float $deltaHours): void + { + $event = Event::where('user_id', auth()->id())->findOrFail($eventId); + $userTz = auth()->user()->timezone; + $minEnd = $event->starts_at->copy()->addMinutes(15); + + $newEnd = $event->ends_at + ? $event->ends_at->copy()->addMinutes((int)round($deltaHours * 60)) + : $event->starts_at->copy()->addMinutes(60); + + if ($newEnd->lt($minEnd)) $newEnd = $minEnd; + + $event->ends_at = $newEnd->copy()->utc(); + $event->end_date = $newEnd->copy()->setTimezone($userTz)->toDateString(); + $event->save(); + } + + public function render() + { + $userTz = auth()->user()->timezone; + + $events = Event::where('user_id', auth()->id()) + ->where(function ($q) { + $q->whereBetween('starts_at', [$this->rangeStart(), $this->rangeEnd()]) + ->orWhereBetween('ends_at', [$this->rangeStart(), $this->rangeEnd()]) + ->orWhere(fn($q2) => $q2 + ->where('starts_at', '<=', $this->rangeStart()) + ->where('ends_at', '>=', $this->rangeEnd()) + ); + }) + ->orderBy('starts_at') + ->get() + ->map(function ($event) use ($userTz) { + $event->starts_local = $event->starts_at->setTimezone($userTz); + $event->ends_local = $event->ends_at?->setTimezone($userTz); + return $event; + }); + + // MehrtΓ€gige Timed-Events (Start- und Enddatum unterschiedlich) gehΓΆren + // zusammen mit ganztΓ€gigen Events in den All-Day-Strip oben. + $allDayEvents = $events->filter(fn($e) => + $e->is_all_day + || ($e->ends_local && $e->starts_local->toDateString() !== $e->ends_local->toDateString()) + ); + $timedEvents = $events->filter(fn($e) => + !$e->is_all_day + && (!$e->ends_local || $e->starts_local->toDateString() === $e->ends_local->toDateString()) + ); + + // Timed Events pro Wochentag – inkl. mehrtΓ€giger Events, die den Tag ΓΌberspannen + $weekDaysList = $this->weekDays; + $eventsByDay = collect(range(0, 6))->map(function ($i) use ($timedEvents, $weekDaysList) { + $dayDate = $weekDaysList[$i]->toDateString(); + return $timedEvents + ->filter(function ($e) use ($dayDate) { + $eStart = $e->starts_local->toDateString(); + $eEnd = $e->ends_local ? $e->ends_local->toDateString() : $eStart; + return $eStart <= $dayDate && $eEnd >= $dayDate; + }) + ->values(); + })->all(); + + // ── All-Day Events: einzelne BlΓΆcke mit spaltenΓΌbergreifenden Positionen ────── + $weekStart = $weekDaysList[0]->toDateString(); + $weekEnd = $weekDaysList[6]->toDateString(); + + // Sortiere nach Startdatum β†’ lΓ€ngere Events bekommen zuerst eine Zeile + $sortedAllDay = $allDayEvents->sortBy(fn($e) => $e->starts_local->toDateString())->values(); + + $rowSlots = []; // $rowSlots[$row][$col] = true β†’ belegt + $allDayRows = []; + + foreach ($sortedAllDay as $e) { + $eStart = $e->starts_local->toDateString(); + $eEnd = $e->ends_local ? $e->ends_local->toDateString() : $eStart; + + // Auf sichtbare Woche clippen + $visStart = max($eStart, $weekStart); + $visEnd = min($eEnd, $weekEnd); + + $startCol = (int) min(6, max(0, $weekDaysList[0]->copy()->diffInDays(Carbon::parse($visStart)))); + $endCol = (int) min(6, max(0, $weekDaysList[0]->copy()->diffInDays(Carbon::parse($visEnd)))); + + // Erste freie Zeile ohne Konflikt suchen + $row = 0; + while (true) { + $conflict = false; + for ($c = $startCol; $c <= $endCol; $c++) { + if (!empty($rowSlots[$row][$c])) { $conflict = true; break; } + } + if (!$conflict) break; + $row++; + } + for ($c = $startCol; $c <= $endCol; $c++) { + $rowSlots[$row][$c] = true; + } + + $allDayRows[] = [ + 'id' => $e->id, + 'title' => $e->title, + 'color' => $e->color, + 'startCol' => $startCol, + 'endCol' => $endCol, + 'row' => $row, + 'startDate' => $eStart, + 'startsInWeek' => $eStart >= $weekStart, + 'endsInWeek' => $eEnd <= $weekEnd, + 'isAllDay' => (bool) $e->is_all_day, + 'startTime' => $e->is_all_day ? null : $e->starts_local->format('H:i'), + 'endTime' => $e->is_all_day ? null : $e->ends_local?->format('H:i'), + ]; + } + + $maxRow = empty($allDayRows) ? 0 : max(array_column($allDayRows, 'row')); + $allDayHeight = ($maxRow + 1) * 26 + 8; // 26 px pro Zeile + 8 px Padding + $hasAllDay = !empty($allDayRows); + + // ── Überlappungs-Layout: gestapeltes Stacking nur bei echter ZeitΓΌberschneidung ── + // Algorithmus: Greedy-Tiefenzuweisung (Intervall-Graph-Coloring) + // depth=0 β†’ volle Breite / kein Offset; depth=N β†’ NΓ—OFFSET_PX links eingerΓΌckt + $layoutsByDay = []; + foreach (range(0, 6) as $dayIndex) { + $dayEvs = collect($eventsByDay[$dayIndex] ?? []) + ->sortBy(fn($e) => $e->starts_local->hour * 60 + $e->starts_local->minute) + ->values(); + + $assigned = []; // ['start', 'end', 'depth'] – bereits verarbeitete Events + $layouts = []; + + foreach ($dayEvs as $event) { + $startLocal = $event->starts_local; + $endLocal = $event->ends_local ?? $startLocal->copy()->addHour(); + $startDec = $startLocal->hour + $startLocal->minute / 60; + $endDec = min($endLocal->hour + $endLocal->minute / 60, 24.0); + + // Tiefen aller zeitlich ΓΌberlappenden VorgΓ€nger sammeln + $usedDepths = []; + foreach ($assigned as $a) { + if ($startDec < $a['end'] && $endDec > $a['start']) { + $usedDepths[] = $a['depth']; + } + } + + // Kleinste freie Tiefe wΓ€hlen + $depth = 0; + while (in_array($depth, $usedDepths)) { + $depth++; + } + + $assigned[] = ['start' => $startDec, 'end' => $endDec, 'depth' => $depth]; + $layouts[$event->id] = ['depth' => $depth]; + } + + $layoutsByDay[$dayIndex] = $layouts; + } + +// return match ($this->view) { +// 'month' => view('livewire.calendar.month-canvas', [ +// 'events' => $events, +// 'currentDate' => $this->currentDate, +// ]), +// +// 'day' => view('livewire.calendar.day-canvas', [ +// 'events' => $events, +// 'currentDate' => $this->currentDate, +// ]), +// +// default => view('livewire.calendar.week-canvas', [ +// 'eventsByDay' => $eventsByDay, +// 'layoutsByDay' => $layoutsByDay, +// 'allDayRows' => $allDayRows, +// 'allDayHeight' => $allDayHeight, +// 'hasAllDay' => $hasAllDay, +// 'weekDays' => $this->weekDays, +// 'hourStart' => 0, +// 'hourEnd' => 24, +// ]) +// }; + + return view('livewire.calendar.week-canvas', [ + 'currentDate' => $this->currentDate, + 'events' => $events, + 'eventsByDay' => $eventsByDay, + 'layoutsByDay' => $layoutsByDay, + 'allDayRows' => $allDayRows, + 'allDayHeight' => $allDayHeight, + 'hasAllDay' => $hasAllDay, + 'weekDays' => $this->weekDays, + 'hourStart' => 0, + 'hourEnd' => 24, + ]); + } +} + +//namespace App\Livewire\Calendar; +// +//use Livewire\Component; +//use App\Models\Event; +//use Carbon\Carbon; +// +//class WeekCanvas extends Component +//{ +// public string $view = 'week'; +// public string $currentDate; +// +// public function mount() +// { +// $this->currentDate = now()->toDateString(); +// } +// +// public function navigate(string $direction) +// { +// $date = Carbon::parse($this->currentDate); +// match ($this->view) { +// 'week' => $direction === 'prev' ? $date->subWeek() : $date->addWeek(), +// 'month' => $direction === 'prev' ? $date->subMonth() : $date->addMonth(), +// 'day' => $direction === 'prev' ? $date->subDay() : $date->addDay(), +// }; +// $this->currentDate = $date->toDateString(); +// } +// +// public function moveEvent(string $eventId, string $newDate, int $newHour): void +// { +// $event = Event::where('user_id', auth()->id())->findOrFail($eventId); +// $userTz = auth()->user()->timezone; +// $duration = $event->ends_at +// ? $event->starts_at->diffInMinutes($event->ends_at) +// : 60; +// +// $newStart = Carbon::parse($newDate, $userTz) +// ->setHour($newHour) +// ->setMinute(0) +// ->setSecond(0) +// ->utc(); +// +// $event->starts_at = $newStart; +// $event->ends_at = $newStart->copy()->addMinutes($duration); +// $event->start_date = $newStart->copy()->setTimezone($userTz)->toDateString(); +// $event->end_date = $newStart->copy()->addMinutes($duration)->setTimezone($userTz)->toDateString(); +// $event->save(); +// } +// +// public function resizeEvent(string $eventId, float $deltaHours): void +// { +// $event = Event::where('user_id', auth()->id())->findOrFail($eventId); +// $userTz = auth()->user()->timezone; +// $minEnd = $event->starts_at->copy()->addMinutes(30); +// +// $newEnd = $event->ends_at +// ? $event->ends_at->copy()->addMinutes((int)($deltaHours * 60)) +// : $event->starts_at->copy()->addMinutes(60); +// +// // Minimum 30 Minuten +// if ($newEnd->lt($minEnd)) { +// $newEnd = $minEnd; +// } +// +// $event->ends_at = $newEnd->utc(); +// $event->end_date = $newEnd->setTimezone($userTz)->toDateString(); +// $event->save(); +// } +// +// public function rangeStart(): Carbon +// { +// $date = Carbon::parse($this->currentDate, auth()->user()->timezone); +// +// return match($this->view) { +// 'month' => $date->copy()->startOfMonth()->startOfWeek(Carbon::MONDAY)->startOfDay()->utc(), +// 'week' => $date->copy()->startOfWeek(Carbon::MONDAY)->startOfDay()->utc(), +// 'day' => $date->copy()->startOfDay()->utc(), +// }; +// } +// +// public function rangeEnd(): Carbon +// { +// $date = Carbon::parse($this->currentDate, auth()->user()->timezone); +// +// return match($this->view) { +// 'month' => $date->copy()->endOfMonth()->endOfWeek(Carbon::SUNDAY)->endOfDay()->utc(), +// 'week' => $date->copy()->endOfWeek(Carbon::SUNDAY)->endOfDay()->utc(), +// 'day' => $date->copy()->endOfDay()->utc(), +// }; +// } +// +// public function render() +// { +// $userTz = auth()->user()->timezone; +// +// $events = Event::where('user_id', auth()->id()) +// ->where(function ($q) { +// // Events die in den Range fallen β€” auch mehrtΓ€gige +// $q->whereBetween('starts_at', [$this->rangeStart(), $this->rangeEnd()]) +// ->orWhereBetween('ends_at', [$this->rangeStart(), $this->rangeEnd()]) +// ->orWhere(function ($q2) { +// // Events die den ganzen Range ΓΌberspannen +// $q2->where('starts_at', '<=', $this->rangeStart()) +// ->where('ends_at', '>=', $this->rangeEnd()); +// }); +// }) +// ->orderBy('starts_at') +// ->get() +// ->map(function ($event) use ($userTz) { +// // UTC β†’ User-Timezone fΓΌr die Anzeige +// $event->starts_at_local = $event->starts_at->setTimezone($userTz); +// $event->ends_at_local = $event->ends_at?->setTimezone($userTz); +// return $event; +// }); +// +// return view('livewire.calendar.week-canvas', [ +// 'events' => $events, +// 'userTz' => $userTz, +// 'weekDays' => $this->getWeekDays(), +// ]); +// } +// +// private function getWeekDays(): array +// { +// $userTz = auth()->user()->timezone; +// $start = Carbon::parse($this->currentDate, $userTz)->startOfWeek(Carbon::MONDAY); +// +// return collect(range(0, 6))->map( +// fn($i) => $start->copy()->addDays($i) +// )->all(); +// } +// +//} +// public $date; +// public $events = []; +// +// public function mount() +// { +// $tz = auth()->user()->timezone; +// +// $this->date = $this->date +// ? Carbon::parse($this->date, $tz) +// : now($tz); +// +// $this->loadEvents(); +// } +// +// public function loadEvents() +// { +// $tz = auth()->user()->timezone; +// $user = auth()->user(); +// +// $start = $this->date->copy()->startOfWeek(); +// $end = $this->date->copy()->endOfWeek(); +// +// $this->events = $user->events() +// ->where(function ($q) use ($start, $end) { +// $q->whereBetween('starts_at', [$start, $end]) +// ->orWhereBetween('ends_at', [$start, $end]) +// ->orWhere(function ($q2) use ($start, $end) { +// $q2->where('starts_at', '<=', $start) +// ->where('ends_at', '>=', $end); +// }); +// }) +// ->get() +// ->map(fn($event) => $this->transformEvent($event, $start, $tz)); +// } +// +// public function updateEventTimeAndDate($id, $date, $start) +// { +// $event = \App\Models\Event::findOrFail($id); +// +// $tz = auth()->user()->timezone; +// +// $start = \Carbon\Carbon::parse("$date $start", $tz); +// +// $duration = $event->ends_at +// ? $event->starts_at->diffInSeconds($event->ends_at) +// : 3600; +// +// $event->update([ +// 'starts_at' => $start->utc(), +// 'ends_at' => $start->copy()->addSeconds($duration)->utc(), +// ]); +// +// $this->loadEvents(); +// } +// +// +// public function updateEventRange($id, $startDate, $endDate, $startTime, $endTime) +// { +// $event = \App\Models\Event::findOrFail($id); +// +// $tz = auth()->user()->timezone; +// +// if (!preg_match('/^\d{2}:\d{2}$/', $startTime)) return; +// if (!preg_match('/^\d{2}:\d{2}$/', $endTime)) return; +// +// $start = \Carbon\Carbon::parse("$startDate $startTime", $tz); +// $end = \Carbon\Carbon::parse("$endDate $endTime", $tz); +// +// // Sicherheit +// if ($end->lessThanOrEqualTo($start)) { +// $end = $start->copy()->addHour(); +// } +// +// $event->update([ +// 'starts_at' => $start->utc(), +// 'ends_at' => $end->utc(), +// ]); +// +// $this->loadEvents(); +// } +// +// private function transformEvent($event, $weekStart, $tz) +// { +// $start = $event->starts_at->copy()->setTimezone($tz); +// $end = $event->ends_at?->copy()->setTimezone($tz) ?? $start->copy()->addHour(); +// +// /* +// |-------------------------------------------------------------------------- +// | POSITION +// |-------------------------------------------------------------------------- +// */ +// +// $dayIndex = $weekStart->diffInDays($start->copy()->startOfDay()); +// $dayIndex = max(0, min(6, $dayIndex)); +// +// $days = $start->copy()->startOfDay()->diffInDays($end->copy()->startOfDay()) + 1; +// +// /* +// |-------------------------------------------------------------------------- +// | TIME +// |-------------------------------------------------------------------------- +// */ +// +// $hourHeight = 64; +// +// $gap = 3; // px Abstand +// +// $top = ($start->hour * $hourHeight) +// + (($start->minute / 60) * $hourHeight) +// + ($gap / 2); +// +// $height = max($start->diffInMinutes($end), 30) / 60 * $hourHeight +// - $gap; +// +// $gap = 0.25; // % +// +// return [ +// 'id' => $event->id, +// 'title' => $event->title, +// 'color' => $event->color, +// 'left' => ($dayIndex / 7) * 100 + $gap, +// 'width' => ($days / 7) * 100 - ($gap * 2), +// 'top' => $top, +// 'height' => $height, +// 'start' => $start, +// 'end' => $end, +// ]; +// } +// +// +// public function colorToRgba($color, $opacity = 1): string +// { +// if (!$color) return 'rgba(99,102,241,0.8)'; // fallback +// +// $color = ltrim($color, '#'); +// +// $r = hexdec(substr($color, 0, 2)); +// $g = hexdec(substr($color, 2, 2)); +// $b = hexdec(substr($color, 4, 2)); +// +// return "rgba($r, $g, $b, $opacity)"; +// } +// +// public function render() +// { +// return view('livewire.calendar.week-canvas'); +// } +//} diff --git a/src/app/Livewire/Checkout/Index.php b/src/app/Livewire/Checkout/Index.php new file mode 100644 index 0000000..9e67755 --- /dev/null +++ b/src/app/Livewire/Checkout/Index.php @@ -0,0 +1,136 @@ +plan = Plan::public()->where('active', true)->findOrFail($planId); + $this->billing = in_array($billing, ['monthly', 'yearly']) ? $billing : 'monthly'; + } + + /** + * 'cancel' β†’ Free Plan gewΓ€hlt: Stripe-Abo kΓΌndigen + * 'update' β†’ User hat aktives Stripe-Abo: in-place updaten (keine Weiterleitung zu Stripe) + * 'new' β†’ Kein aktives Abo: neuer Stripe Checkout + */ + #[Computed] + public function checkoutType(): string + { + if ($this->plan->isFree()) { + return 'cancel'; + } + + return $this->activeStripeSub ? 'update' : 'new'; + } + + #[Computed] + public function activeStripeSub(): ?Subscription + { + return Subscription::where('user_id', auth()->id()) + ->where('provider', 'stripe') + ->whereNotNull('provider_subscription_id') + ->where('status', SubscriptionStatus::Active->value) + ->latest('created_at') + ->first(); + } + + public function startCheckout(): void + { + $user = auth()->user(); + $stripe = app(StripeService::class); + + try { + // ── Free Plan: Stripe-Abo kΓΌndigen ──────────────────────────── + if ($this->plan->isFree()) { + $stripe->cancelUserSubscription($user); + $this->redirect(route('subscription.index'), navigate: false); + return; + } + + // ── Upgrade / Downgrade: bestehendes Abo in-place updaten ───── + $existingSub = $this->activeStripeSub; + if ($existingSub) { + $stripe->updateStripeSubscription( + $existingSub->provider_subscription_id, + $this->plan, + $this->billing, + $user->id + ); + $this->redirect(route('subscription.index'), navigate: false); + return; + } + + // ── Erstes Abo: neuer Stripe Checkout ───────────────────────── + $session = $stripe->createCheckoutSession($user, $this->plan, $this->billing); + $this->redirect($session->url, navigate: false); + + } catch (\Throwable $e) { + $this->dispatch('notify', ['type' => 'error', 'message' => 'Fehler: ' . $e->getMessage()]); + } + } + + public function openBillingPortal(): void + { + $customerId = \App\Models\Subscription::where('user_id', auth()->id()) + ->where('provider', 'stripe') + ->whereNotNull('provider_customer_id') + ->latest('created_at') + ->value('provider_customer_id'); + + if (!$customerId) { + $this->dispatch('notify', ['type' => 'error', 'message' => 'Kein Stripe-Konto verknΓΌpft.']); + return; + } + + try { + $url = app(StripeService::class)->createBillingPortalSession($customerId); + $this->redirect($url, navigate: false); + } catch (\Throwable $e) { + $this->dispatch('notify', ['type' => 'error', 'message' => 'Fehler beim Γ–ffnen des Portals.']); + } + } + + #[Computed] + public function monthlyPrice(): int + { + return $this->plan->getMonthlyPrice(); + } + + #[Computed] + public function yearlyPrice(): int + { + return $this->plan->getYearlyPrice(); + } + + #[Computed] + public function activePrice(): int + { + return $this->billing === 'yearly' ? $this->yearlyPrice : $this->monthlyPrice; + } + + #[Computed] + public function savings(): int + { + return ($this->monthlyPrice * 12) - $this->yearlyPrice; + } + + public function render() + { + return view('livewire.checkout.index', [ + 'features' => $this->plan->features()->with('group')->orderBy('sort')->get(), + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Checkout/Success.php b/src/app/Livewire/Checkout/Success.php new file mode 100644 index 0000000..8ba8ea1 --- /dev/null +++ b/src/app/Livewire/Checkout/Success.php @@ -0,0 +1,65 @@ +since = now()->subMinutes(1)->toDateTimeString(); + $this->checkStatus(); + } + + public function checkStatus(): void + { + if ($this->done) { + return; + } + + $userId = auth()->id(); + + $sub = Subscription::where('user_id', $userId) + ->where('provider', 'stripe') + ->where('status', SubscriptionStatus::Active->value) + ->latest('created_at') + ->first(); + + $this->subscriptionReady = $sub !== null; + + if ($sub) { + // PrimΓ€r: Payment direkt an dieser Subscription + $this->paymentReady = Payment::where('subscription_id', $sub->id) + ->where('status', PaymentStatus::Paid->value) + ->exists(); + + // Fallback: irgendein paid-Payment des Users seit Seitenaufruf + // (fΓ€ngt Race-Conditions ab wo Payment an anderer Sub-ID hΓ€ngt) + if (!$this->paymentReady) { + $this->paymentReady = Payment::where('user_id', $userId) + ->where('status', PaymentStatus::Paid->value) + ->where('created_at', '>=', $this->since) + ->exists(); + } + } + + $this->done = $this->subscriptionReady && $this->paymentReady; + } + + public function render() + { + return view('livewire.checkout.success') + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Contacts/Index.php b/src/app/Livewire/Contacts/Index.php new file mode 100644 index 0000000..d2cfa2f --- /dev/null +++ b/src/app/Livewire/Contacts/Index.php @@ -0,0 +1,154 @@ +resetPage(); } + public function updatedFilterType(): void { $this->resetPage(); } + + // ── Panel ───────────────────────────────────────────────────────────── + + public function openCreate(): void + { + $this->reset(['editId', 'name', 'email', 'phone', 'notes', 'birthday']); + $this->type = ''; + $this->panelOpen = true; + } + + public function openEdit(string $id): void + { + $contact = Contact::forUser(auth()->id())->findOrFail($id); + + $this->editId = $id; + $this->name = $contact->name; + $this->email = $contact->email ?? ''; + $this->phone = $contact->phone ?? ''; + $this->type = $contact->type ?? ''; + $this->notes = $contact->notes ?? ''; + $this->birthday = $contact->birthday?->format('Y-m-d') ?? ''; + + $this->panelOpen = true; + } + + public function closePanel(): void + { + $this->reset(['panelOpen', 'editId', 'name', 'email', 'phone', 'type', 'notes', 'birthday']); + } + + // ── Speichern ───────────────────────────────────────────────────────── + + public function save(): void + { + $this->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['nullable', 'email', 'max:255'], + 'phone' => ['nullable', 'string', 'max:50'], + 'type' => ['nullable', 'in:' . implode(',', Contact::TYPES)], + 'notes' => ['nullable', 'string', 'max:2000'], + 'birthday' => ['nullable', 'date'], + ]); + + $data = [ + 'name' => $this->name, + 'email' => $this->email ?: null, + 'phone' => $this->phone ?: null, + 'type' => $this->type ?: null, + 'notes' => $this->notes ?: null, + 'birthday' => $this->birthday ?: null, + ]; + + if ($this->editId) { + Contact::forUser(auth()->id())->where('id', $this->editId)->update($data); + + Activity::log( + auth()->id(), + Activity::TYPE_CONTACT_UPDATED, + Activity::localizedTitle('contact_updated', auth()->user()->locale ?? 'de'), + $this->name, + ); + + $this->dispatch('notify', ['type' => 'success', 'message' => 'Kontakt gespeichert']); + } else { + Contact::create(array_merge($data, ['user_id' => auth()->id()])); + + Activity::log( + auth()->id(), + Activity::TYPE_CONTACT_CREATED, + Activity::localizedTitle('contact_created', auth()->user()->locale ?? 'de'), + $this->name, + ); + + $this->dispatch('notify', ['type' => 'success', 'message' => 'Kontakt erstellt']); + } + + $this->closePanel(); + } + + // ── LΓΆschen ─────────────────────────────────────────────────────────── + + public function delete(string $id): void + { + $contact = Contact::forUser(auth()->id())->findOrFail($id); + $name = $contact->name; + $contact->delete(); + + Activity::log(auth()->id(), Activity::TYPE_CONTACT_UPDATED, Activity::localizedTitle('contact_deleted', auth()->user()->locale ?? 'de'), $name); + } + + // ── Render ──────────────────────────────────────────────────────────── + + public function render() + { + $userId = auth()->id(); + + $query = Contact::forUser($userId)->orderBy('name'); + + if ($this->search !== '') { + $query->search($this->search); + } + + if ($this->filterType !== '') { + $query->where('type', $this->filterType); + } + + $contacts = $query->paginate(20); + + $stats = Contact::forUser($userId) + ->selectRaw(" + count(*) as total, + sum(case when type = 'privat' then 1 else 0 end) as privat, + sum(case when type = 'arbeit' then 1 else 0 end) as arbeit, + sum(case when type = 'kunde' then 1 else 0 end) as kunde, + sum(case when type = 'sonstiges' then 1 else 0 end) as sonstiges + ") + ->first(); + + return view('livewire.contacts.index', [ + 'contacts' => $contacts, + 'stats' => $stats, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Dashboard/Index.php b/src/app/Livewire/Dashboard/Index.php new file mode 100644 index 0000000..deb891a --- /dev/null +++ b/src/app/Livewire/Dashboard/Index.php @@ -0,0 +1,141 @@ +locale = auth()->user()->locale ?? 'de'; + $tz = auth()->user()->timezone; + $today = now($tz)->format('m-d'); + + $this->birthdaysToday = Contact::where('user_id', auth()->id()) + ->whereNotNull('birthday') + ->get() + ->filter(fn ($c) => Carbon::parse($c->birthday)->format('m-d') === $today) + ->values(); + + $this->birthdaysSoon = Contact::where('user_id', auth()->id()) + ->whereNotNull('birthday') + ->get() + ->filter(function ($c) use ($tz) { + $days = now($tz)->diffInDays( + Carbon::parse($c->birthday)->setYear(now($tz)->year), + false + ); + return $days > 0 && $days <= 7; + }) + ->sortBy(fn ($c) => Carbon::parse($c->birthday)->format('m-d')) + ->values(); + } + + public function render() + { + $user = auth()->user(); + $userId = $user->id; + $tz = $user->timezone ?? 'UTC'; + + $today = now($tz)->toDateString(); + $todayDate = now($tz)->startOfDay()->utc(); + $todayEnd = now($tz)->endOfDay()->utc(); + + // ── Termine heute (inkl. mehrtΓ€gige und ganztΓ€gige Überschneidungen) ─ + $todayEvents = Event::where('user_id', $userId) + ->where('starts_at', '<=', $todayEnd) // startet vor/wΓ€hrend heute + ->where('ends_at', '>=', $todayDate) // endet nach/wΓ€hrend heute + ->orderByDesc('is_all_day') // Ganztag-Termine zuerst + ->orderBy('starts_at') + ->limit(8) + ->get(); + + // ── NΓ€chste Termine (ab jetzt) ──────────────────────────────────── + $upcomingEvents = Event::where('user_id', $userId) + ->where('starts_at', '>', now()->utc()) + ->orderBy('starts_at') + ->limit(3) + ->get(); + + // ── Offene Aufgaben (fΓ€llig heute oder ΓΌberfΓ€llig + ohne Datum) ─── + $openTasks = Task::forUser($userId) + ->where('status', '!=', Task::STATUS_DONE) + ->orderByRaw("FIELD(priority, 'high', 'medium', 'low')") + ->orderBy('due_at') + ->limit(6) + ->get(); + + // ── Letzte Notizen ──────────────────────────────────────────────── + $recentNotes = Note::forUser($userId) + ->orderByDesc('pinned') + ->orderByDesc('created_at') + ->limit(3) + ->get(); + + // ── Letzte AktivitΓ€ten ──────────────────────────────────────────── + $recentActivities = Activity::forUser($userId) + ->latest() + ->limit(7) + ->get(); + + // ── Statistiken ─────────────────────────────────────────────────── + $stats = [ + 'events_today' => $todayEvents->count(), + 'open_tasks' => Task::forUser($userId)->where('status', '!=', Task::STATUS_DONE)->count(), + 'notes_total' => Note::forUser($userId)->count(), + 'credits_month' => AgentLog::where('user_id', $userId) + ->where('created_at', '>=', now()->startOfMonth()) + ->sum('credits'), + 'overdue_tasks' => Task::forUser($userId) + ->where('status', '!=', Task::STATUS_DONE) + ->whereNotNull('due_at') + ->whereDate('due_at', '<', $today) + ->count(), + ]; + + // ── Plan & Credits ──────────────────────────────────────────────── + $subscription = $user->subscription()->with('plan')->first() + ?? $user->latestSubscription()->with('plan')->first(); + $plan = $subscription?->plan; + $creditLimit = $user->effective_limit; + $usagePercent = $user->usage_percent; + + // ── Begrüßung ───────────────────────────────────────────────────── + $hour = now($tz)->hour; + $greetingKey = match (true) { + $hour >= 5 && $hour < 12 => 'dashboard.greeting_morning', + $hour >= 12 && $hour < 18 => 'dashboard.greeting_afternoon', + default => 'dashboard.greeting_evening', + }; + $greeting = t($greetingKey, [], $this->locale); + + return view('livewire.dashboard.index', [ + 'user' => $user, + 'firstName' => explode(' ', $user->name)[0], + 'greeting' => $greeting, + 'tz' => $tz, + 'todayEvents' => $todayEvents, + 'upcomingEvents' => $upcomingEvents, + 'openTasks' => $openTasks, + 'recentNotes' => $recentNotes, + 'recentActivities' => $recentActivities, + 'stats' => $stats, + 'plan' => $plan, + 'subscription' => $subscription, + 'creditLimit' => $creditLimit, + 'usagePercent' => $usagePercent, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Homepage/Agb.php b/src/app/Livewire/Homepage/Agb.php new file mode 100644 index 0000000..68cf92c --- /dev/null +++ b/src/app/Livewire/Homepage/Agb.php @@ -0,0 +1,14 @@ +layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Homepage/Datenschutz.php b/src/app/Livewire/Homepage/Datenschutz.php new file mode 100644 index 0000000..d808d03 --- /dev/null +++ b/src/app/Livewire/Homepage/Datenschutz.php @@ -0,0 +1,14 @@ +layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Homepage/Example.php b/src/app/Livewire/Homepage/Example.php new file mode 100644 index 0000000..668a288 --- /dev/null +++ b/src/app/Livewire/Homepage/Example.php @@ -0,0 +1,25 @@ +validate(['notifyEmail' => 'required|email']); + $this->notifySubmitted = true; + } + + public function render() + { + return view('livewire.homepage.example', [ + 'userCount' => User::count(), + ])->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Homepage/Impressum.php b/src/app/Livewire/Homepage/Impressum.php new file mode 100644 index 0000000..70069cd --- /dev/null +++ b/src/app/Livewire/Homepage/Impressum.php @@ -0,0 +1,14 @@ +layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Homepage/Index.php b/src/app/Livewire/Homepage/Index.php new file mode 100644 index 0000000..e3f80b5 --- /dev/null +++ b/src/app/Livewire/Homepage/Index.php @@ -0,0 +1,21 @@ +check()) { + return redirect()->route('dashboard.index'); + } + } + + public function render() + { + return view('livewire.homepage.index') + ->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Homepage/Kontakt.php b/src/app/Livewire/Homepage/Kontakt.php new file mode 100644 index 0000000..98ce471 --- /dev/null +++ b/src/app/Livewire/Homepage/Kontakt.php @@ -0,0 +1,36 @@ + 'required|min:2|max:100', + 'email' => 'required|email|max:255', + 'betreff' => 'required|min:3|max:200', + 'nachricht' => 'required|min:10|max:5000', + ]; + + public function send() + { + $this->validate(); + + // TODO: Mail senden oder in DB speichern + $this->sent = true; + $this->reset(['name', 'email', 'betreff', 'nachricht']); + } + + public function render() + { + return view('livewire.homepage.kontakt') + ->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Homepage/Preise.php b/src/app/Livewire/Homepage/Preise.php new file mode 100644 index 0000000..024b529 --- /dev/null +++ b/src/app/Livewire/Homepage/Preise.php @@ -0,0 +1,30 @@ +check()) { + return redirect()->route('plans.index'); + } + + $this->plans = Plan::public()->where('active', true) + ->with(['features', 'features.group']) + ->orderBy('sort') + ->get(); + } + + public function render() + { + return view('livewire.homepage.preise') + ->layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Homepage/UeberUns.php b/src/app/Livewire/Homepage/UeberUns.php new file mode 100644 index 0000000..5ebe914 --- /dev/null +++ b/src/app/Livewire/Homepage/UeberUns.php @@ -0,0 +1,14 @@ +layout('layouts.blank'); + } +} diff --git a/src/app/Livewire/Integration/Index.php b/src/app/Livewire/Integration/Index.php new file mode 100644 index 0000000..2388b23 --- /dev/null +++ b/src/app/Livewire/Integration/Index.php @@ -0,0 +1,127 @@ +activeProvider = $provider; + $integration = $this->getIntegration($provider); + $this->syncMode = $integration?->sync_mode ?? 'read'; + $this->modalOpen = true; + } + + public function closeModal(): void + { + $this->modalOpen = false; + $this->activeProvider = ''; + } + + public function updateSyncMode(string $mode): void + { + if (!$this->canUseSyncMode()) return; + + CalendarIntegration::where('user_id', auth()->id()) + ->where('provider', $this->activeProvider) + ->update(['sync_mode' => $mode]); + + $this->syncMode = $mode; + + // Watch (Push-Benachrichtigungen) registrieren wenn Pull-Modus aktiv + if ($this->activeProvider === 'google' && in_array($mode, ['read', 'both'])) { + $integration = $this->getIntegration('google'); + if ($integration) { + app(GoogleCalendarService::class)->createWatch($integration->fresh()); + } + } + } + + public function disconnect(): void + { + $integration = $this->getIntegration($this->activeProvider); + + // Watch stoppen bevor wir die Integration lΓΆschen + if ($integration && $this->activeProvider === 'google') { + app(GoogleCalendarService::class)->stopWatch($integration); + } + + CalendarIntegration::where('user_id', auth()->id()) + ->where('provider', $this->activeProvider) + ->delete(); + + $this->closeModal(); + } + + /** + * Manueller Sync-Button: sofort Pull von Google starten. + */ + public function syncNow(): void + { + $integration = $this->getIntegration('google'); + + if (!$integration || !in_array($integration->sync_mode, ['read', 'both'])) { + return; + } + + SyncFromGoogleCalendarJob::dispatch($integration->id); + + $this->dispatch('notify', [ + 'type' => 'success', + 'message' => 'Sync gestartet β€” Termine werden gleich importiert.', + ]); + } + + /** + * Watch registrieren (bei Sync-Mode-Γ„nderung auf read|both oder manuell). + */ + public function registerWatch(): void + { + $integration = $this->getIntegration('google'); + if (!$integration) return; + + app(GoogleCalendarService::class)->createWatch($integration); + + $this->dispatch('notify', [ + 'type' => 'success', + 'message' => 'Push-Benachrichtigungen aktiviert.', + ]); + } + + // ── Helpers ────────────────────────────────────────────────────────── + + public function getIntegration(string $provider): ?CalendarIntegration + { + return auth()->user()->calendarIntegration($provider); + } + + public function canUseSyncMode(): bool + { + return auth()->user()->hasFeature('calendar_sync'); + } + + public function render() + { + $google = $this->getIntegration('google'); + $outlook = $this->getIntegration('outlook'); + + return view('livewire.integration.index', [ + 'google' => $google, + 'outlook' => $outlook, + 'canSync' => $this->canUseSyncMode(), + 'justConnected' => session('integration_connected'), + 'connectionError' => session('integration_error'), + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Integration/Modals/Form.php b/src/app/Livewire/Integration/Modals/Form.php new file mode 100644 index 0000000..a1157f8 --- /dev/null +++ b/src/app/Livewire/Integration/Modals/Form.php @@ -0,0 +1,13 @@ +user(); + + $subscription = $user->subscription()->with('plan')->first() + ?? $user->latestSubscription()->with('plan')->first(); + + $openPayments = Payment::where('user_id', $user->id) + ->whereIn('status', [PaymentStatus::Pending->value, PaymentStatus::Failed->value]) + ->with('subscription') + ->latest() + ->get(); + + $payments = Payment::where('user_id', $user->id) + ->with('subscription') + ->latest() + ->paginate(15); + + $daysLeft = $subscription?->ends_at + ? (int) now()->diffInDays($subscription->ends_at, false) + : null; + + return view('livewire.invoices.index', [ + 'subscription' => $subscription, + 'openPayments' => $openPayments, + 'payments' => $payments, + 'daysLeft' => $daysLeft, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Notes/Index.php b/src/app/Livewire/Notes/Index.php new file mode 100644 index 0000000..ece1eb7 --- /dev/null +++ b/src/app/Livewire/Notes/Index.php @@ -0,0 +1,126 @@ +reset(['editId', 'title', 'content']); + $this->color = 'yellow'; + $this->showForm = true; + } + + public function openEdit(string $id): void + { + $note = Note::forUser(auth()->id())->findOrFail($id); + + $this->editId = $id; + $this->title = $note->title ?? ''; + $this->content = $note->content; + $this->color = $note->color; + $this->showForm = true; + } + + public function save(): void + { + $this->validate([ + 'content' => ['required', 'string', 'max:5000'], + 'title' => ['nullable', 'string', 'max:255'], + 'color' => ['required', 'in:' . implode(',', Note::COLORS)], + ]); + + if ($this->editId) { + Note::forUser(auth()->id())->where('id', $this->editId)->update([ + 'title' => $this->title ?: null, + 'content' => $this->content, + 'color' => $this->color, + ]); + } else { + Note::create([ + 'user_id' => auth()->id(), + 'title' => $this->title ?: null, + 'content' => $this->content, + 'color' => $this->color, + ]); + + Activity::log( + auth()->id(), + Activity::TYPE_NOTE_CREATED, + Activity::localizedTitle('note_created', auth()->user()->locale ?? 'de'), + $this->title ?: null, + ); + } + + $this->closeForm(); + $this->dispatch('notify', ['type' => 'success', 'message' => $this->editId ? 'Notiz gespeichert' : 'Notiz erstellt']); + } + + public function closeForm(): void + { + $this->reset(['showForm', 'editId', 'title', 'content']); + $this->color = 'yellow'; + } + + // ── Pin / LΓΆschen ───────────────────────────────────────────────────── + + public function togglePin(string $id): void + { + $note = Note::forUser(auth()->id())->findOrFail($id); + $note->update(['pinned' => !$note->pinned]); + } + + public function delete(string $id): void + { + Note::forUser(auth()->id())->where('id', $id)->delete(); + + Activity::log(auth()->id(), Activity::TYPE_NOTE_DELETED, Activity::localizedTitle('note_deleted', auth()->user()->locale ?? 'de')); + } + + // ── Render ──────────────────────────────────────────────────────────── + + public function render() + { + $userId = auth()->id(); + + $query = Note::forUser($userId)->orderByDesc('pinned')->orderByDesc('created_at'); + + if ($this->search !== '') { + $query->search($this->search); + } + + if ($this->filterColor !== '') { + $query->where('color', $this->filterColor); + } + + $notes = $query->get(); + + $stats = [ + 'total' => Note::forUser($userId)->count(), + 'pinned' => Note::forUser($userId)->where('pinned', true)->count(), + ]; + + return view('livewire.notes.index', [ + 'notes' => $notes, + 'stats' => $stats, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Notifications/Bell.php b/src/app/Livewire/Notifications/Bell.php new file mode 100644 index 0000000..0a7c712 --- /dev/null +++ b/src/app/Livewire/Notifications/Bell.php @@ -0,0 +1,99 @@ +loadNotifications(); + } + + public function refresh(): void + { + $userId = auth()->id(); + $newCount = Notification::where('user_id', $userId)->whereNull('read_at')->count(); + + if ($newCount > $this->unreadCount) { + $this->dispatch('new-notification'); + } + + $this->loadNotifications(); + } + + public function markRead(string $id): void + { + Notification::where('user_id', auth()->id()) + ->where('id', $id) + ->update(['read_at' => now()]); + + $this->loadNotifications(); + } + + public function markAllRead(): void + { + Notification::where('user_id', auth()->id()) + ->whereNull('read_at') + ->update(['read_at' => now()]); + + $this->loadNotifications(); + } + + public function deleteNotification(string $id): void + { + Notification::where('user_id', auth()->id()) + ->where('id', $id) + ->delete(); + + $this->loadNotifications(); + } + + public function deleteAllRead(): void + { + Notification::where('user_id', auth()->id()) + ->whereNotNull('read_at') + ->delete(); + + $this->loadNotifications(); + } + + public function deleteAll(): void + { + Notification::where('user_id', auth()->id())->delete(); + $this->loadNotifications(); + } + + private function loadNotifications(): void + { + $userId = auth()->id(); + + $this->unreadCount = Notification::where('user_id', $userId) + ->whereNull('read_at') + ->count(); + + $this->notifications = Notification::where('user_id', $userId) + ->latest() + ->take(15) + ->get() + ->map(fn ($n) => [ + 'id' => $n->id, + 'type' => $n->type, + 'title' => $n->title, + 'message' => $n->message, + 'read_at' => $n->read_at?->toISOString(), + 'created_at' => $n->created_at->toISOString(), + ]) + ->toArray(); + } + + public function render() + { + return view('livewire.notifications.bell'); + } +} diff --git a/src/app/Livewire/Payments/Index.php b/src/app/Livewire/Payments/Index.php new file mode 100644 index 0000000..b297063 --- /dev/null +++ b/src/app/Livewire/Payments/Index.php @@ -0,0 +1,13 @@ +plans = Plan::public()->with(['features.group']) + ->where('active', true) + ->orderBy('sort') + ->get(); + + $this->allFeatures = \App\Models\Feature::with('group') + ->where('active', true) + ->orderBy('sort') + ->get(); + + $user = Auth::user(); + + if ($user?->subscription) { + $this->subscription = $user->subscription; + $this->currentPlanKey = $user->subscription->plan?->plan_key; + } + } + + public function render() + { + return view('livewire.plans.index') + ->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Settings/Index.php b/src/app/Livewire/Settings/Index.php new file mode 100644 index 0000000..dab9481 --- /dev/null +++ b/src/app/Livewire/Settings/Index.php @@ -0,0 +1,386 @@ + 'testSmtp', + ]; + + // ── Mount ───────────────────────────────────────────────────────────── + + public function mount(): void + { + $user = auth()->user(); + $settings = $user->settings ?? []; + + $this->name = $user->name; + $this->email = $user->email; + $this->timezone = $user->timezone ?? 'UTC'; + $this->locale = $user->locale ?? app()->getLocale(); + + $this->reminders_enabled = (bool) ($settings['reminders_enabled'] ?? true); + $this->notify_in_app = (bool) ($settings['notify_in_app'] ?? true); + $this->notify_email = (bool) ($settings['notify_email'] ?? false); + + $notifSettings = $user->notification_settings ?? []; + $this->pushEnabled = (bool) ($notifSettings['push_enabled'] ?? false); + $this->emailEnabled = (bool) ($notifSettings['email_enabled'] ?? false); + + $this->smtp_host = $settings['smtp_host'] ?? null; + $this->smtp_port = $settings['smtp_port'] ?? null; + $this->smtp_user = $settings['smtp_user'] ?? null; + $this->smtp_password = ($settings['smtp_password'] ?? null) ? '*******' : ''; + $this->smtp_encryption = $settings['smtp_encryption'] ?? 'tls'; + + $this->emailsSentToday = $user->emailLogs() + ->whereDate('created_at', today()) + ->count(); + } + + // ── Profil speichern ────────────────────────────────────────────────── + + public function saveProfile(): void + { + $this->validate( + [ + 'name' => ['required'], + 'email' => ['required', 'email'], + 'timezone' => ['required', 'timezone'], + 'locale' => ['required', 'in:' . implode(',', array_keys(config('app.locales')))], + ], + [ + 'name.required' => t('settings.validation.name_required'), + 'email.required' => t('settings.validation.email_required'), + 'email.email' => t('settings.validation.email_invalid'), + 'locale.required' => t('settings.validation.locale_required'), + 'locale.in' => t('settings.validation.locale_invalid'), + ] + ); + + auth()->user()->update([ + 'name' => $this->name, + 'email' => $this->email, + 'timezone' => $this->timezone, + 'locale' => $this->locale, + ]); + + app()->setLocale($this->locale); + session(['locale' => $this->locale]); + + $this->dispatch('notify', ['type' => 'success', 'message' => t('settings.profile.saved')]); + } + + // ── Benachrichtigungen + SMTP speichern ─────────────────────────────── + + public function saveSettings(): void + { + $user = auth()->user(); + $settings = $user->settings ?? []; + + $user->update([ + 'settings' => [ + 'reminders_enabled' => $this->reminders_enabled, + 'notify_in_app' => $this->notify_in_app, + 'notify_email' => $this->notify_email, + + 'smtp_host' => $this->smtp_host, + 'smtp_port' => $this->smtp_port, + 'smtp_user' => $this->smtp_user, + 'smtp_encryption' => $this->smtp_encryption, + + // Passwort nur ΓΌberschreiben wenn geΓ€ndert + 'smtp_password' => ($this->smtp_password && $this->smtp_password !== '*******') + ? encrypt($this->smtp_password) + : ($settings['smtp_password'] ?? null), + ], + ]); + + $this->dispatch('notify', ['type' => 'success', 'message' => t('settings.saved')]); + } + + // ── Passwort Γ€ndern ─────────────────────────────────────────────────── + + public function updatePassword(): void + { + $this->validate([ + 'current_password' => ['required'], + 'new_password' => ['required', 'min:6', 'confirmed'], + ], [ + 'current_password.required' => t('settings.validation.current_password_required'), + 'new_password.required' => t('settings.validation.new_password_required'), + 'new_password.min' => t('settings.validation.new_password_min'), + 'new_password.confirmed' => t('settings.validation.new_password_confirmed'), + ]); + + $user = auth()->user(); + + if (!Hash::check($this->current_password, $user->password)) { + $this->addError('current_password', t('settings.validation.current_password_wrong')); + return; + } + + $user->update(['password' => Hash::make($this->new_password)]); + + $this->reset(['current_password', 'new_password', 'new_password_confirmation']); + + $this->dispatch('notify', ['type' => 'success', 'message' => t('settings.password.saved')]); + } + + // ── Push / Email Toggle ─────────────────────────────────────────────── + + public function updatedPushEnabled(): void + { + $this->saveNotificationSettings(); + } + + public function updatedEmailEnabled(): void + { + $this->saveNotificationSettings(); + } + + private function saveNotificationSettings(): void + { + $user = auth()->user(); + + $user->update([ + 'notification_settings' => [ + 'push_enabled' => $this->pushEnabled, + 'email_enabled' => $this->emailEnabled, + ], + ]); + + // Devices aktivieren/deaktivieren je nach Push-Wunsch + $user->devices()->update(['active' => $this->pushEnabled]); + } + + // ── SMTP Test (via Mailable) ────────────────────────────────────────── + + public function sendTestMail(): void + { + $user = auth()->user(); + + if (!RateLimiter::attempt('smtp-test:' . $user->id, 3, function () {})) { + $this->smtpStatus = 'error'; + $this->smtpError = 'Bitte warte 1 Minute zwischen Tests.'; + return; + } + + try { + $settings = $user->settings ?? []; + $config = $user->smtp_settings ?? []; + + $host = $config['host'] ?? $settings['smtp_host'] ?? null; + + if (empty($host)) { + $this->smtpStatus = 'error'; + $this->smtpError = 'Kein SMTP Server konfiguriert.'; + return; + } + + $password = ($this->smtp_password && $this->smtp_password !== '*******') + ? $this->smtp_password + : (isset($settings['smtp_password']) ? decrypt($settings['smtp_password']) : null); + + if (!$password) { + $this->smtpStatus = 'error'; + $this->smtpError = 'SMTP-Passwort fehlt.'; + return; + } + + Config::set('mail.mailers.user_smtp', [ + 'transport' => 'smtp', + 'host' => $this->smtp_host ?? $host, + 'port' => (int) ($this->smtp_port ?? $config['port'] ?? 587), + 'encryption' => $this->smtp_encryption === 'null' ? null : ($this->smtp_encryption ?? 'tls'), + 'username' => $this->smtp_user ?? $config['username'], + 'password' => $password, + 'from' => [ + 'address' => $config['from_address'] ?? $user->email, + 'name' => $config['from_name'] ?? $user->name, + ], + 'timeout' => 10, + ]); + + Mail::mailer('user_smtp') + ->to($user->email) + ->send(new \App\Mail\SmtpTestMail($user)); + + $this->smtpStatus = 'success'; + + } catch (\Throwable $e) { + $this->smtpStatus = 'error'; + $this->smtpError = $e->getMessage(); + } + } + + // ── SMTP testen (Legacy β€” dispatch) ────────────────────────────────── + + public function testSmtp(): void + { + if (!$this->smtp_host || !$this->smtp_port || !$this->smtp_user) { + $this->dispatch('notify', ['type' => 'error', 'message' => t('settings.smtp.invalid_config')]); + return; + } + + try { + $settings = auth()->user()->settings ?? []; + + $password = ($this->smtp_password && $this->smtp_password !== '*******') + ? $this->smtp_password + : (isset($settings['smtp_password']) ? decrypt($settings['smtp_password']) : null); + + if (!$password) { + throw new \RuntimeException('SMTP-Passwort fehlt.'); + } + + Config::set('mail.default', 'smtp'); + Config::set('mail.mailers.smtp', [ + 'transport' => 'smtp', + 'host' => $this->smtp_host, + 'port' => (int) $this->smtp_port, + 'username' => $this->smtp_user, + 'password' => $password, + 'encryption' => $this->smtp_encryption === 'null' ? null : $this->smtp_encryption, + 'timeout' => 10, + ]); + + Mail::raw(t('settings.smtp.test_mail_body'), function ($msg) { + $msg->to(auth()->user()->email)->subject(t('settings.smtp.test_mail_subject')); + }); + + $this->dispatch('notify', ['type' => 'success', 'message' => t('settings.smtp.success')]); + + } catch (\Throwable $e) { + $this->dispatch('openModal', 'settings.modals.smtp-error', ['message' => $e->getMessage()]); + } + } + + // ── Affiliate ───────────────────────────────────────────────────────── + + public function joinAffiliate(): void + { + $user = auth()->user(); + + if ($user->isInternalUser() || $user->affiliate) { + return; + } + + Affiliate::create([ + 'user_id' => $user->id, + 'code' => Affiliate::generateCode($user), + 'status' => 'active', + ]); + + $this->dispatch('notify', ['type' => 'success', 'message' => 'Du nimmst jetzt am Affiliate-Programm teil!']); + } + + // ── Computed ────────────────────────────────────────────────────────── + + public function getCanTestSmtpProperty(): bool + { + return (bool) ($this->smtp_host + && $this->smtp_port + && $this->smtp_user + && ($this->smtp_password || isset(auth()->user()->settings['smtp_password']))); + } + + public function render() + { + $affiliate = auth()->user()->affiliate; + $referrals = $affiliate?->referrals() + ->with('referredUser') + ->latest() + ->get() ?? collect(); + + $user = auth()->user(); + + // Echte Nutzung aus agent_logs (Chat, Aktionen) β€” Label priorisiert + // output.title (z.B. "Friseurtermin Huber") vor dem rohen User-Input. + $logItems = $user->agentLogs() + ->latest() + ->limit(20) + ->get() + ->map(fn ($log) => (object) [ + 'source' => 'log', + 'type' => $log->type ?? 'chat', + 'label' => $log->output['title'] ?? $log->input ?? $log->type, + 'amount' => -$log->credits, + 'duration_ms' => $log->duration_ms, + 'created_at' => $log->created_at, + ]); + + // Bonus-Bewegungen aus credit_transactions + $txItems = $user->creditTransactions() + ->latest() + ->limit(20) + ->get() + ->map(fn ($tx) => (object) [ + 'source' => 'transaction', + 'type' => $tx->type, + 'label' => $tx->description, + 'amount' => $tx->amount, + 'duration_ms' => null, + 'created_at' => $tx->created_at, + ]); + + // ZusammenfΓΌhren, chronologisch, auf 20 begrenzen + $creditTransactions = $logItems + ->concat($txItems) + ->sortByDesc('created_at') + ->take(20) + ->values(); + + $creditBalance = $user->credit_balance; + $planLimit = $user->subscription?->plan?->credit_limit ?? 0; + $bonusLeft = $user->bonus_credits; + $monthUsage = $user->monthly_usage; + $effLimit = $user->effective_limit; + + return view('livewire.settings.index', compact( + 'affiliate', 'referrals', 'creditTransactions', + 'creditBalance', 'planLimit', 'bonusLeft', 'monthUsage', 'effLimit' + ))->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Settings/Modals/DeleteAccount.php b/src/app/Livewire/Settings/Modals/DeleteAccount.php new file mode 100644 index 0000000..329f581 --- /dev/null +++ b/src/app/Livewire/Settings/Modals/DeleteAccount.php @@ -0,0 +1,42 @@ +validate([ + 'password' => ['required'], + ], [ + 'password.required' => t('settings.validation.password_required'), + ]); + + $user = auth()->user(); + + if (!Hash::check($this->password, $user->password)) { + $this->addError('password', t('settings.validation.current_password_wrong')); + return; + } + + Auth::logout(); + + request()->session()->invalidate(); + request()->session()->regenerateToken(); + + $user->requestDeletion(); + + return redirect()->route('homepage.index'); + } + + public function render() + { + return view('livewire.settings.modals.delete-account'); + } +} diff --git a/src/app/Livewire/Settings/Modals/SmtpError.php b/src/app/Livewire/Settings/Modals/SmtpError.php new file mode 100644 index 0000000..6147153 --- /dev/null +++ b/src/app/Livewire/Settings/Modals/SmtpError.php @@ -0,0 +1,19 @@ +message = $message; + } + + public function render() + { + return view('livewire.settings.modals.smtp-error'); + } +} diff --git a/src/app/Livewire/Subscription/Index.php b/src/app/Livewire/Subscription/Index.php new file mode 100644 index 0000000..c312b2c --- /dev/null +++ b/src/app/Livewire/Subscription/Index.php @@ -0,0 +1,43 @@ +redirect(route('checkout.index', [ + 'planId' => $planId, + 'billing' => $this->billing, + ])); + } + + public function render() + { + $user = auth()->user(); + $subscription = $user->subscription()->with('plan')->first() + ?? $user->latestSubscription()->with('plan')->first(); + + $plans = Plan::public()->with(['features.group']) + ->where('active', true) + ->orderBy('sort') + ->get(); + + $allFeatures = Feature::with('group') + ->where('active', true) + ->orderBy('sort') + ->get(); + + return view('livewire.subscription.index', [ + 'subscription' => $subscription, + 'plans' => $plans, + 'allFeatures' => $allFeatures, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Livewire/Tasks/Index.php b/src/app/Livewire/Tasks/Index.php new file mode 100644 index 0000000..c28599c --- /dev/null +++ b/src/app/Livewire/Tasks/Index.php @@ -0,0 +1,210 @@ +reset(['editId', 'taskTitle', 'description', 'due_at', 'reminderAt']); + $this->priority = 'medium'; + $this->showForm = true; + } + + public function openEdit(string $id): void + { + $task = Task::forUser(auth()->id())->findOrFail($id); + + $tz = auth()->user()->timezone ?? 'Europe/Vienna'; + + $this->editId = $id; + $this->taskTitle = $task->title; + $this->description = $task->description ?? ''; + $this->priority = $task->priority; + $this->due_at = $task->due_at ? $task->due_at->format('Y-m-d') : ''; + $this->reminderAt = $task->reminder_at + ? $task->reminder_at->setTimezone($tz)->format('Y-m-d\TH:i') + : ''; + $this->showForm = true; + } + + public function save(): void + { + $this->validate([ + 'taskTitle' => ['required', 'string', 'max:255'], + 'description' => ['nullable', 'string', 'max:2000'], + 'priority' => ['required', 'in:low,medium,high'], + 'due_at' => ['nullable', 'date'], + 'reminderAt' => ['nullable', 'string'], + ]); + + if ($this->reminderAt) { + $isPro = auth()->user()->subscription?->plan?->plan_key !== 'free'; + + if (!$isPro) { + $query = Task::where('user_id', auth()->id()) + ->whereNotNull('reminder_at') + ->where('reminder_at', '>', now()) + ->where('reminder_sent', false) + ->where('status', '!=', 'done'); + + if ($this->editId) { + $query->where('id', '!=', $this->editId); + } + + if ($query->count() >= 3) { + $this->addError('reminderAt', 'Limit erreicht: Max. 3 aktive Erinnerungen im Free Plan.'); + return; + } + } + } + + $tz = auth()->user()->timezone ?? 'Europe/Vienna'; + $reminderUtc = $this->reminderAt + ? \Carbon\Carbon::createFromFormat('Y-m-d\TH:i', $this->reminderAt, $tz)->utc() + : null; + + $data = [ + 'title' => $this->taskTitle, + 'description' => $this->description ?: null, + 'priority' => $this->priority, + 'due_at' => $this->due_at ?: null, + 'reminder_at' => $reminderUtc, + 'reminder_sent' => false, + ]; + + $isEdit = (bool) $this->editId; + + if ($isEdit) { + Task::forUser(auth()->id())->where('id', $this->editId)->update($data); + } else { + Task::create(array_merge($data, ['user_id' => auth()->id()])); + + Activity::log( + auth()->id(), + Activity::TYPE_TASK_CREATED, + Activity::localizedTitle('task_created', auth()->user()->locale ?? 'de'), + $this->taskTitle, + meta: ['priority' => $this->priority, 'due_at' => $this->due_at ?: null] + ); + } + + $this->closeForm(); + $this->dispatch('notify', ['type' => 'success', 'message' => $isEdit ? 'Aufgabe gespeichert' : 'Aufgabe erstellt']); + } + + public function closeForm(): void + { + $this->reset(['showForm', 'editId', 'taskTitle', 'description', 'due_at', 'reminderAt']); + $this->priority = 'medium'; + } + + public function setReminderQuick(int $minutes): void + { + $tz = auth()->user()->timezone ?? 'Europe/Vienna'; + $this->reminderAt = now($tz)->addMinutes($minutes)->format('Y-m-d\TH:i'); + } + + public function setReminderTomorrow(): void + { + $tz = auth()->user()->timezone ?? 'Europe/Vienna'; + $this->reminderAt = now($tz)->addDay()->setHour(8)->setMinute(0)->format('Y-m-d\TH:i'); + } + + public function clearReminder(): void + { + $this->reminderAt = ''; + } + + // ── Status / LΓΆschen ───────────────────────────────────────────────── + + public function toggleDone(string $id): void + { + $task = Task::forUser(auth()->id())->findOrFail($id); + + if ($task->isDone()) { + $task->update(['status' => Task::STATUS_PENDING, 'completed_at' => null]); + } else { + $task->update(['status' => Task::STATUS_DONE, 'completed_at' => now()]); + + Activity::log( + auth()->id(), + Activity::TYPE_TASK_COMPLETED, + Activity::localizedTitle('task_completed', auth()->user()->locale ?? 'de'), + $task->title, + ); + } + } + + public function setStatus(string $id, string $status): void + { + Task::forUser(auth()->id())->where('id', $id)->update(['status' => $status]); + } + + public function delete(string $id): void + { + $task = Task::forUser(auth()->id())->where('id', $id)->first(); + if ($task) { + Activity::log(auth()->id(), Activity::TYPE_TASK_DELETED, Activity::localizedTitle('task_deleted', auth()->user()->locale ?? 'de'), $task->title); + $task->delete(); + } + } + + // ── Render ──────────────────────────────────────────────────────────── + + public function render() + { + $userId = auth()->id(); + + $query = Task::forUser($userId)->orderByRaw("FIELD(priority, 'high', 'medium', 'low')")->orderBy('due_at')->orderByDesc('created_at'); + + if ($this->search !== '') { + $query->search($this->search); + } + + if ($this->filterStatus !== '') { + $query->ofStatus($this->filterStatus); + } + + $tasks = $query->get(); + $openTasks = $tasks->where('status', '!=', 'done')->values(); + $doneTasks = $tasks->where('status', 'done')->values(); + + $stats = Task::forUser($userId) + ->selectRaw(" + count(*) as total, + sum(case when status != 'done' then 1 else 0 end) as open, + sum(case when status = 'done' then 1 else 0 end) as done, + sum(case when DATE(due_at) = CURRENT_DATE and status != 'done' then 1 else 0 end) as today + ") + ->first(); + + return view('livewire.tasks.index', [ + 'tasks' => $tasks, + 'openTasks' => $openTasks, + 'doneTasks' => $doneTasks, + 'stats' => $stats, + ])->layout('layouts.app'); + } +} diff --git a/src/app/Mail/AffiliateQualifiedMail.php b/src/app/Mail/AffiliateQualifiedMail.php new file mode 100644 index 0000000..bd2c315 --- /dev/null +++ b/src/app/Mail/AffiliateQualifiedMail.php @@ -0,0 +1,37 @@ +credits} Credits verdient!", + ); + } + + public function content(): Content + { + return new Content( + view: 'emails.affiliate-qualified', + ); + } +} diff --git a/src/app/Mail/AriaComposedMail.php b/src/app/Mail/AriaComposedMail.php new file mode 100644 index 0000000..1cfb017 --- /dev/null +++ b/src/app/Mail/AriaComposedMail.php @@ -0,0 +1,34 @@ +subject); + } + + public function content(): Content + { + return new Content( + view: 'emails.aria-composed', + with: ['body' => $this->body], + ); + } +} diff --git a/src/app/Mail/GiftAccessMail.php b/src/app/Mail/GiftAccessMail.php new file mode 100644 index 0000000..91ec564 --- /dev/null +++ b/src/app/Mail/GiftAccessMail.php @@ -0,0 +1,40 @@ +token . '?email=' . urlencode($this->user->email)); + + return new Content( + view: 'emails.reset-password', + with: ['url' => $url, 'user' => $this->user], + ); + } +} diff --git a/src/app/Mail/SmtpTestMail.php b/src/app/Mail/SmtpTestMail.php new file mode 100644 index 0000000..82aa18d --- /dev/null +++ b/src/app/Mail/SmtpTestMail.php @@ -0,0 +1,29 @@ + 'array', + ]; + + protected static function boot(): void + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + // ── Relations ───────────────────────────────────────────────────────── + + public function user() + { + return $this->belongsTo(User::class); + } + + public function subject() + { + return $this->morphTo(); + } + + // ── Scopes ──────────────────────────────────────────────────────────── + + public function scopeForUser(Builder $query, string $userId): Builder + { + return $query->where('user_id', $userId); + } + + public function scopeSearch(Builder $query, string $term): Builder + { + return $query->where(function ($q) use ($term) { + $q->where('title', 'like', "%{$term}%") + ->orWhere('description', 'like', "%{$term}%"); + }); + } + + public function scopeOfType(Builder $query, string $type): Builder + { + // Gruppen-Alias auflΓΆsen + $map = [ + 'event' => ['event_created', 'event_updated', 'event_deleted'], + 'reminder' => ['reminder_sent'], + 'automation' => ['automation_triggered'], + 'contact' => ['contact_created', 'contact_updated'], + 'integration' => ['integration_connected'], + 'note' => ['note_created', 'note_deleted'], + 'task' => ['task_created', 'task_completed', 'task_deleted'], + 'system' => ['settings_changed', 'login'], + ]; + + $types = $map[$type] ?? [$type]; + + return $query->whereIn('type', $types); + } + + // ── Helpers ─────────────────────────────────────────────────────────── + + /** Liefert Icon-Name, Hintergrund- und Textfarbe fΓΌr den Typ. */ + public function visual(): array + { + return match (true) { + str_starts_with($this->type, 'event') => ['icon' => 'calendar', 'bg' => 'bg-indigo-50', 'text' => 'text-indigo-600'], + str_starts_with($this->type, 'reminder') => ['icon' => 'bell', 'bg' => 'bg-amber-50', 'text' => 'text-amber-600'], + str_starts_with($this->type, 'automation') => ['icon' => 'bolt', 'bg' => 'bg-purple-50', 'text' => 'text-purple-600'], + str_starts_with($this->type, 'contact') => ['icon' => 'user', 'bg' => 'bg-green-50', 'text' => 'text-green-600'], + str_starts_with($this->type, 'integration') => ['icon' => 'link', 'bg' => 'bg-blue-50', 'text' => 'text-blue-600'], + str_starts_with($this->type, 'note') => ['icon' => 'document-text', 'bg' => 'bg-amber-50', 'text' => 'text-amber-600'], + str_starts_with($this->type, 'task') => ['icon' => 'check-circle', 'bg' => 'bg-teal-50', 'text' => 'text-teal-600'], + $this->type === 'login' => ['icon' => 'arrow-right-on-rectangle', 'bg' => 'bg-gray-100', 'text' => 'text-gray-500'], + default => ['icon' => 'cog-6-tooth', 'bg' => 'bg-gray-100', 'text' => 'text-gray-500'], + }; + } + + /** Menschlich lesbarer Typ-Label (lokalisiert). */ + public function typeLabel(): string + { + $locale = app()->getLocale(); + $labels = [ + 'event_created' => ['de' => 'Termin erstellt', 'en' => 'Event created'], + 'event_updated' => ['de' => 'Termin bearbeitet', 'en' => 'Event updated'], + 'event_deleted' => ['de' => 'Termin gelΓΆscht', 'en' => 'Event deleted'], + 'reminder_sent' => ['de' => 'Erinnerung gesendet', 'en' => 'Reminder sent'], + 'automation_triggered' => ['de' => 'Automation ausgefΓΌhrt', 'en' => 'Automation triggered'], + 'contact_created' => ['de' => 'Kontakt erstellt', 'en' => 'Contact created'], + 'contact_updated' => ['de' => 'Kontakt bearbeitet', 'en' => 'Contact updated'], + 'integration_connected' => ['de' => 'Integration verbunden', 'en' => 'Integration connected'], + 'settings_changed' => ['de' => 'Einstellungen geΓ€ndert', 'en' => 'Settings changed'], + 'login' => ['de' => 'Anmeldung', 'en' => 'Login'], + 'note_created' => ['de' => 'Notiz erstellt', 'en' => 'Note created'], + 'note_deleted' => ['de' => 'Notiz gelΓΆscht', 'en' => 'Note deleted'], + 'task_created' => ['de' => 'Aufgabe erstellt', 'en' => 'Task created'], + 'task_completed' => ['de' => 'Aufgabe erledigt', 'en' => 'Task completed'], + 'task_deleted' => ['de' => 'Aufgabe gelΓΆscht', 'en' => 'Task deleted'], + ]; + return $labels[$this->type][$locale] ?? $labels[$this->type]['de'] ?? $this->type; + } + + /** Lokalisierter AktivitΓ€ts-Titel fΓΌr Activity::log()-Aufrufe. */ + public static function localizedTitle(string $key, string $locale = 'de'): string + { + $texts = [ + 'task_created_assistant' => ['de' => 'Aufgabe via Aria erstellt', 'en' => 'Task created via Aria'], + 'event_created_assistant' => ['de' => 'Termin via Aria erstellt', 'en' => 'Event created via Aria'], + 'note_created_assistant' => ['de' => 'Notiz via Aria erstellt', 'en' => 'Note created via Aria'], + 'contact_created_assistant'=> ['de' => 'Kontakt via Aria erstellt', 'en' => 'Contact created via Aria'], + 'task_created' => ['de' => 'Aufgabe erstellt', 'en' => 'Task created'], + 'task_completed' => ['de' => 'Aufgabe erledigt', 'en' => 'Task completed'], + 'task_deleted' => ['de' => 'Aufgabe gelΓΆscht', 'en' => 'Task deleted'], + 'event_created' => ['de' => 'Termin erstellt', 'en' => 'Event created'], + 'event_updated' => ['de' => 'Termin aktualisiert', 'en' => 'Event updated'], + 'event_deleted' => ['de' => 'Termin gelΓΆscht', 'en' => 'Event deleted'], + 'contact_created' => ['de' => 'Kontakt erstellt', 'en' => 'Contact created'], + 'contact_updated' => ['de' => 'Kontakt bearbeitet', 'en' => 'Contact updated'], + 'contact_deleted' => ['de' => 'Kontakt gelΓΆscht', 'en' => 'Contact deleted'], + 'note_created' => ['de' => 'Notiz erstellt', 'en' => 'Note created'], + 'note_deleted' => ['de' => 'Notiz gelΓΆscht', 'en' => 'Note deleted'], + ]; + return $texts[$key][$locale] ?? $texts[$key]['de'] ?? $key; + } + + // ── Static Factory ──────────────────────────────────────────────────── + + public static function log( + string $userId, + string $type, + string $title, + ?string $description = null, + ?Model $subject = null, + array $meta = [] + ): self { + return self::create([ + 'user_id' => $userId, + 'type' => $type, + 'title' => $title, + 'description' => $description, + 'subject_type' => $subject ? get_class($subject) : null, + 'subject_id' => $subject?->id, + 'meta' => $meta ?: null, + ]); + } +} diff --git a/src/app/Models/Affiliate.php b/src/app/Models/Affiliate.php new file mode 100644 index 0000000..40cf388 --- /dev/null +++ b/src/app/Models/Affiliate.php @@ -0,0 +1,71 @@ +id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function referrals(): HasMany + { + return $this->hasMany(AffiliateReferral::class); + } + + public function creditLogs(): HasMany + { + return $this->hasMany(AffiliateCreditLog::class); + } + + public function getReferralLinkAttribute(): string + { + return 'https://www.aziros.com/register?ref=' . $this->code; + } + + public static function generateCode(User $user): string + { + // Format: AZ-XXXXXX (6 alphanumerische Zeichen, ohne verwechselbare Buchstaben) + $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // ohne I, O, 0, 1 + + do { + $random = ''; + for ($i = 0; $i < 6; $i++) { + $random .= $chars[random_int(0, strlen($chars) - 1)]; + } + $code = 'AZ-' . $random; + } while (static::where('code', $code)->exists()); + + return $code; + } +} diff --git a/src/app/Models/AffiliateCreditLog.php b/src/app/Models/AffiliateCreditLog.php new file mode 100644 index 0000000..7ce55ad --- /dev/null +++ b/src/app/Models/AffiliateCreditLog.php @@ -0,0 +1,36 @@ +id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function affiliate(): BelongsTo + { + return $this->belongsTo(Affiliate::class); + } +} diff --git a/src/app/Models/AffiliateReferral.php b/src/app/Models/AffiliateReferral.php new file mode 100644 index 0000000..5b1acff --- /dev/null +++ b/src/app/Models/AffiliateReferral.php @@ -0,0 +1,51 @@ + 'datetime', + 'qualifies_at' => 'datetime', + 'paid_at' => 'datetime', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function affiliate(): BelongsTo + { + return $this->belongsTo(Affiliate::class); + } + + public function referredUser(): BelongsTo + { + return $this->belongsTo(User::class, 'referred_user_id'); + } +} diff --git a/src/app/Models/AgentLog.php b/src/app/Models/AgentLog.php new file mode 100644 index 0000000..0258915 --- /dev/null +++ b/src/app/Models/AgentLog.php @@ -0,0 +1,66 @@ + 'array', + 'output' => 'array', + + // πŸ”₯ Token + Metrics Casting + 'prompt_tokens' => 'integer', + 'completion_tokens' => 'integer', + 'total_tokens' => 'integer', + 'credits' => 'integer', + 'duration_ms' => 'integer', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + // πŸ”₯ Relation + public function user() + { + return $this->belongsTo(User::class); + } + +} diff --git a/src/app/Models/ApiToken.php b/src/app/Models/ApiToken.php new file mode 100644 index 0000000..a9c0407 --- /dev/null +++ b/src/app/Models/ApiToken.php @@ -0,0 +1,31 @@ + 'datetime', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/src/app/Models/AppVersion.php b/src/app/Models/AppVersion.php new file mode 100644 index 0000000..68e5647 --- /dev/null +++ b/src/app/Models/AppVersion.php @@ -0,0 +1,43 @@ + 'boolean', + 'released_at' => 'datetime', + ]; + + protected static function boot(): void + { + parent::boot(); + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public static function current(string $platform = 'web'): ?self + { + return static::where('status', 'live') + ->whereIn('platform', [$platform, 'all']) + ->latest('released_at') + ->first(); + } +} diff --git a/src/app/Models/Automation.php b/src/app/Models/Automation.php new file mode 100644 index 0000000..a4d73eb --- /dev/null +++ b/src/app/Models/Automation.php @@ -0,0 +1,59 @@ + 'boolean', + 'config' => 'array', + 'last_run_at' => 'datetime', + ]; + + protected static function boot(): void + { + parent::boot(); + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + /** Alias fΓΌr config β€” fΓΌr konsistente Lesbarkeit mit der API-Antwort. */ + public function getSettingsAttribute(): array + { + return $this->config ?? []; + } + + public function setSettingsAttribute(array $value): void + { + $this->config = $value; + } + + /** Gibt einen config-Wert zurΓΌck, mit Fallback auf den globalen Default. */ + public function cfg(string $key, mixed $default = null): mixed + { + return $this->config[$key] ?? $default; + } +} diff --git a/src/app/Models/CalendarIntegration.php b/src/app/Models/CalendarIntegration.php new file mode 100644 index 0000000..81da51f --- /dev/null +++ b/src/app/Models/CalendarIntegration.php @@ -0,0 +1,74 @@ + 'datetime', + 'last_synced_at' => 'datetime', + 'watch_expires_at' => 'datetime', + ]; + + protected $hidden = [ + 'access_token', + 'refresh_token', + 'sync_token', + ]; + + protected static function boot(): void + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function isExpired(): bool + { + return $this->token_expires_at && $this->token_expires_at->isPast(); + } + + /** Gibt den access_token unverschlΓΌsselt zurΓΌck (umgeht $hidden). */ + public function rawAccessToken(): ?string + { + return $this->getRawOriginal('access_token'); + } + + /** Gibt den sync_token zurΓΌck (umgeht $hidden). */ + public function rawSyncToken(): ?string + { + return $this->getRawOriginal('sync_token'); + } +} diff --git a/src/app/Models/Contact.php b/src/app/Models/Contact.php new file mode 100644 index 0000000..a217bfd --- /dev/null +++ b/src/app/Models/Contact.php @@ -0,0 +1,94 @@ + 'date:Y-m-d', + ]; + + const TYPES = ['privat', 'arbeit', 'kunde', 'sonstiges']; + + protected static function boot(): void + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + // ── Scopes ──────────────────────────────────────────────────────────── + + public function scopeForUser(Builder $query, string $userId): Builder + { + return $query->where('user_id', $userId); + } + + public function scopeSearch(Builder $query, string $term): Builder + { + return $query->where(function ($q) use ($term) { + $q->where('name', 'like', "%{$term}%") + ->orWhere('email', 'like', "%{$term}%") + ->orWhere('phone', 'like', "%{$term}%"); + }); + } + + // ── Helpers ─────────────────────────────────────────────────────────── + + public function initials(): string + { + $parts = explode(' ', trim($this->name)); + if (count($parts) >= 2) { + return strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1)); + } + return strtoupper(substr($this->name, 0, 2)); + } + + public function typeLabel(): string + { + return match ($this->type) { + 'privat' => 'Privat', + 'arbeit' => 'Arbeit', + 'kunde' => 'Kunde', + 'sonstiges' => 'Sonstiges', + default => 'Kontakt', + }; + } + + public function typeClasses(): array + { + return match ($this->type) { + 'privat' => ['bg' => 'bg-blue-50', 'text' => 'text-blue-700', 'avatar' => 'bg-blue-100 text-blue-700'], + 'arbeit' => ['bg' => 'bg-indigo-50', 'text' => 'text-indigo-700', 'avatar' => 'bg-indigo-100 text-indigo-700'], + 'kunde' => ['bg' => 'bg-green-50', 'text' => 'text-green-700', 'avatar' => 'bg-green-100 text-green-700'], + 'sonstiges' => ['bg' => 'bg-gray-50', 'text' => 'text-gray-600', 'avatar' => 'bg-gray-100 text-gray-600'], + default => ['bg' => 'bg-purple-50', 'text' => 'text-purple-700', 'avatar' => 'bg-purple-100 text-purple-700'], + }; + } +} diff --git a/src/app/Models/CreditTransaction.php b/src/app/Models/CreditTransaction.php new file mode 100644 index 0000000..590f352 --- /dev/null +++ b/src/app/Models/CreditTransaction.php @@ -0,0 +1,96 @@ + 'integer', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + // ── Factory-Methoden ────────────────────────────────────────────── + + public static function onboarding(User $user, int $amount = 300): self + { + return self::create([ + 'user_id' => $user->id, + 'amount' => $amount, + 'type' => 'onboarding', + 'description' => 'Willkommens-Bonus bei Registrierung', + ]); + } + + public static function affiliate(User $user, int $amount, string $referredUserName, string $referralId): self + { + return self::create([ + 'user_id' => $user->id, + 'amount' => $amount, + 'type' => 'affiliate', + 'description' => "Referral-Bonus: {$referredUserName} ist seit 3 Monaten aktiv", + 'reference_id' => $referralId, + 'reference_type' => 'affiliate_referral', + ]); + } + + public static function adminGift(User $user, int $amount, string $reason, User $admin): self + { + return self::create([ + 'user_id' => $user->id, + 'amount' => $amount, + 'type' => 'admin_gift', + 'description' => "Admin-Geschenk: {$reason}", + 'created_by' => $admin->id, + ]); + } + + public static function refund(User $user, int $amount, string $reason, string $logId, User $admin): self + { + return self::create([ + 'user_id' => $user->id, + 'amount' => $amount, + 'type' => 'refund', + 'description' => "RΓΌckerstattung: {$reason}", + 'reference_id' => $logId, + 'reference_type' => 'agent_log', + 'created_by' => $admin->id, + ]); + } +} diff --git a/src/app/Models/Device.php b/src/app/Models/Device.php new file mode 100644 index 0000000..e246575 --- /dev/null +++ b/src/app/Models/Device.php @@ -0,0 +1,38 @@ +id) { + $model->id = (string)Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/src/app/Models/EmailLog.php b/src/app/Models/EmailLog.php new file mode 100644 index 0000000..366b18c --- /dev/null +++ b/src/app/Models/EmailLog.php @@ -0,0 +1,19 @@ +belongsTo(User::class); + } +} diff --git a/src/app/Models/Event.php b/src/app/Models/Event.php new file mode 100644 index 0000000..1280d32 --- /dev/null +++ b/src/app/Models/Event.php @@ -0,0 +1,121 @@ + 'datetime', + 'ends_at' => 'datetime', + 'start_date' => 'datetime', + 'end_date' => 'datetime', + 'is_all_day' => 'boolean', + 'reminders' => 'array', + 'exceptions' => 'array', + 'participants' => 'array', + 'recurrence_end_date' => 'date:Y-m-d', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function users() + { + return $this->belongsToMany(User::class, 'event_user'); + } + + public function contacts() + { + return $this->belongsToMany(\App\Models\Contact::class, 'event_contact'); + } + + public function isMultiDay(): bool + { + if (!$this->ends_at) { + return false; + } + + return $this->starts_at->toDateString() !== $this->ends_at->toDateString(); + } + + + public function spansMultipleDays(): bool + { + return $this->ends_at + && !$this->starts_at->isSameDay($this->ends_at); + } + + public function getDisplayTimeForDate(string $date, string $tz): array + { + $start = $this->starts_at->timezone($tz); + $end = $this->ends_at?->timezone($tz); + + $displayStart = $start; + $displayEnd = $end; + + $exceptions = $this->exceptions; + + if (is_string($exceptions)) { + $exceptions = json_decode($exceptions, true); + } + + if (is_array($exceptions)) { + foreach ($exceptions as $ex) { + + if (($ex['date'] ?? null) === $date) { + + if (!empty($ex['start'])) { + $displayStart = $start->copy()->setTimeFromTimeString($ex['start']); + } + + if (!empty($ex['end'])) { + $displayEnd = $start->copy()->setTimeFromTimeString($ex['end']); + } + + } + } + } + + return [ + 'start' => $displayStart, + 'end' => $displayEnd, + ]; + } +} diff --git a/src/app/Models/EventDurationStat.php b/src/app/Models/EventDurationStat.php new file mode 100644 index 0000000..de023a6 --- /dev/null +++ b/src/app/Models/EventDurationStat.php @@ -0,0 +1,30 @@ +id) { + $model->id = (string) Str::uuid(); + } + }); + } + +} diff --git a/src/app/Models/EventKeyword.php b/src/app/Models/EventKeyword.php new file mode 100644 index 0000000..e6ef63d --- /dev/null +++ b/src/app/Models/EventKeyword.php @@ -0,0 +1,29 @@ +id) { + $model->id = (string) Str::uuid(); + } + }); + } +} diff --git a/src/app/Models/Feature.php b/src/app/Models/Feature.php new file mode 100644 index 0000000..705bb77 --- /dev/null +++ b/src/app/Models/Feature.php @@ -0,0 +1,53 @@ + 'boolean', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function group() + { + return $this->belongsTo(FeatureGroup::class, 'feature_group_id'); + } + + public function plans() + { + return $this->belongsToMany( + \App\Models\Plan::class, + 'feature_plan', + 'feature_id', + 'plan_id' + )->withTimestamps(false); + } + +} diff --git a/src/app/Models/FeatureGroup.php b/src/app/Models/FeatureGroup.php new file mode 100644 index 0000000..e4d3441 --- /dev/null +++ b/src/app/Models/FeatureGroup.php @@ -0,0 +1,35 @@ +id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function features() + { + return $this->hasMany(Feature::class); + } + +} diff --git a/src/app/Models/MailQueue.php b/src/app/Models/MailQueue.php new file mode 100644 index 0000000..1ca216d --- /dev/null +++ b/src/app/Models/MailQueue.php @@ -0,0 +1,49 @@ + 'array', + 'available_at' => 'datetime', + 'sent_at' => 'datetime', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + +} diff --git a/src/app/Models/Note.php b/src/app/Models/Note.php new file mode 100644 index 0000000..ad210b7 --- /dev/null +++ b/src/app/Models/Note.php @@ -0,0 +1,69 @@ + 'boolean', + ]; + + const COLORS = ['yellow', 'blue', 'green', 'pink', 'purple', 'gray']; + + protected static function boot(): void + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function scopeForUser(Builder $query, string $userId): Builder + { + return $query->where('user_id', $userId); + } + + public function scopeSearch(Builder $query, string $term): Builder + { + return $query->where(function ($q) use ($term) { + $q->where('title', 'like', "%{$term}%") + ->orWhere('content', 'like', "%{$term}%"); + }); + } + + /** Tailwind bg + text classes fΓΌr die Notizfarbe */ + public function colorClasses(): array + { + return match ($this->color) { + 'yellow' => ['bg' => 'bg-amber-50', 'border' => 'border-amber-200', 'text' => 'text-amber-800'], + 'blue' => ['bg' => 'bg-blue-50', 'border' => 'border-blue-200', 'text' => 'text-blue-800'], + 'green' => ['bg' => 'bg-green-50', 'border' => 'border-green-200', 'text' => 'text-green-800'], + 'pink' => ['bg' => 'bg-pink-50', 'border' => 'border-pink-200', 'text' => 'text-pink-800'], + 'purple' => ['bg' => 'bg-purple-50', 'border' => 'border-purple-200', 'text' => 'text-purple-800'], + default => ['bg' => 'bg-gray-50', 'border' => 'border-gray-200', 'text' => 'text-gray-800'], + }; + } +} diff --git a/src/app/Models/Notification.php b/src/app/Models/Notification.php new file mode 100644 index 0000000..fe22a3f --- /dev/null +++ b/src/app/Models/Notification.php @@ -0,0 +1,28 @@ + 'datetime', + 'data' => 'array', + ]; + + protected static function boot(): void + { + parent::boot(); + static::creating(fn ($model) => $model->id ??= (string) Str::uuid()); + static::created(fn ($n) => broadcast(new \App\Events\NotificationCreated($n))); + } +} diff --git a/src/app/Models/Payment.php b/src/app/Models/Payment.php new file mode 100644 index 0000000..7740ec2 --- /dev/null +++ b/src/app/Models/Payment.php @@ -0,0 +1,100 @@ + 'datetime', + 'status' => PaymentStatus::class, + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + // RELATIONS + public function user() + { + return $this->belongsTo(User::class); + } + + public function subscription() + { + return $this->belongsTo(Subscription::class); + } + + // HELPER + public function isPaid(): bool + { + return $this->status === PaymentStatus::Paid; + } + + /** + * Lesbares Label fΓΌr den Rechnungsgrund. + * billing_reason in DB kann sein: + * - "subscription_create" β†’ Neues Abonnement + * - "subscription_cycle" β†’ VerlΓ€ngerung + * - "subscription_update" β†’ Planwechsel (generisch, Fallback) + * - "upgrade:Pro" β†’ Upgrade zu Pro + * - "downgrade:Basic" β†’ Downgrade zu Basic + * - "manual" β†’ Manuelle Zahlung + */ + public function actionLabel(): string + { + $reason = $this->billing_reason ?? ''; + + if (str_starts_with($reason, 'upgrade:')) { + return 'Upgrade zu ' . substr($reason, 8); + } + + if (str_starts_with($reason, 'downgrade:')) { + return 'Downgrade zu ' . substr($reason, 10); + } + + if (str_starts_with($reason, 'refund:')) { + return 'Erstattung'; + } + + return match ($reason) { + 'subscription_create' => 'Neues Abonnement', + 'subscription_cycle' => 'VerlΓ€ngerung', + 'subscription_update' => 'Planwechsel', + 'manual' => 'Manuelle Zahlung', + default => $this->subscription?->plan_name ?? 'Abonnement', + }; + } +} diff --git a/src/app/Models/Plan.php b/src/app/Models/Plan.php new file mode 100644 index 0000000..745bcae --- /dev/null +++ b/src/app/Models/Plan.php @@ -0,0 +1,101 @@ + 'boolean', + 'is_featured' => 'boolean', + 'is_internal' => 'boolean', + 'ai_config' => 'array', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + // RELATION + public function subscriptions() + { + return $this->hasMany(Subscription::class); + } + + + public function features() + { + return $this->belongsToMany( + \App\Models\Feature::class, + 'feature_plan', + 'plan_id', + 'feature_id' + )->withTimestamps(false); + } + + public function getAiModel(): string + { + return $this->ai_config['model'] + ?? $this->ai_model + ?? 'gpt-4o-mini'; + } + + public function isFree(): bool + { + return $this->price === 0 || $this->price === null; + } + + public function scopePublic($query) + { + return $query->where(function ($q) { + $q->where('is_internal', false)->orWhereNull('is_internal'); + }); + } + + public static function freePlan(): ?self + { + return static::public() + ->where(function ($q) { + $q->where('price', 0)->orWhereNull('price'); + }) + ->first(); + } + + public function getMonthlyPrice() + { + return $this->price; + } + + public function getYearlyPrice() + { + $months = 12 - $this->yearly_discount_months; + return $this->price * $months; + } + +} diff --git a/src/app/Models/Reminder.php b/src/app/Models/Reminder.php new file mode 100644 index 0000000..a157f28 --- /dev/null +++ b/src/app/Models/Reminder.php @@ -0,0 +1,43 @@ + 'datetime', + 'is_sent' => 'boolean', + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/src/app/Models/Subscription.php b/src/app/Models/Subscription.php new file mode 100644 index 0000000..cf9df39 --- /dev/null +++ b/src/app/Models/Subscription.php @@ -0,0 +1,90 @@ + 'datetime', + 'ends_at' => 'datetime', + 'gifted_at' => 'datetime', + 'status' => SubscriptionStatus::class, + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + // RELATIONS + public function user() + { + return $this->belongsTo(User::class); + } + + public function plan() + { + return $this->belongsTo(Plan::class); + } + + public function payments() + { + return $this->hasMany(Payment::class); + } + + public function gifter() + { + return $this->belongsTo(User::class, 'gifted_by'); + } + + // HELPER + public function isActive() + { + return in_array($this->status->value, ['active', 'gifted']) + && (!$this->ends_at || $this->ends_at->isFuture()); + } + + public function isGifted(): bool + { + return $this->status === SubscriptionStatus::Gifted; + } +} diff --git a/src/app/Models/Task.php b/src/app/Models/Task.php new file mode 100644 index 0000000..d12e732 --- /dev/null +++ b/src/app/Models/Task.php @@ -0,0 +1,119 @@ + 'datetime', + 'completed_at' => 'datetime', + 'reminder_at' => 'datetime', + 'reminder_sent' => 'boolean', + ]; + + const STATUS_PENDING = 'pending'; + const STATUS_IN_PROGRESS = 'in_progress'; + const STATUS_DONE = 'done'; + + const PRIORITY_LOW = 'low'; + const PRIORITY_MEDIUM = 'medium'; + const PRIORITY_HIGH = 'high'; + + protected static function boot(): void + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function scopeForUser(Builder $query, string $userId): Builder + { + return $query->where('user_id', $userId); + } + + public function scopeSearch(Builder $query, string $term): Builder + { + return $query->where(function ($q) use ($term) { + $q->where('title', 'like', "%{$term}%") + ->orWhere('description', 'like', "%{$term}%"); + }); + } + + public function scopeOfStatus(Builder $query, string $status): Builder + { + if ($status === 'today') { + return $query->whereDate('due_at', today()) + ->where('status', '!=', self::STATUS_DONE); + } + + return $query->where('status', $status); + } + + public function isDone(): bool + { + return $this->status === self::STATUS_DONE; + } + + public function isOverdue(): bool + { + return $this->due_at + && $this->due_at->isPast() + && !$this->isDone(); + } + + /** Tailwind classes fΓΌr PrioritΓ€t */ + public function priorityClasses(): array + { + return match ($this->priority) { + 'high' => ['dot' => 'bg-red-500', 'badge' => 'bg-red-50 text-red-700'], + 'low' => ['dot' => 'bg-gray-300', 'badge' => 'bg-gray-50 text-gray-500'], + default => ['dot' => 'bg-amber-400', 'badge' => 'bg-amber-50 text-amber-700'], + }; + } + + public function priorityLabel(): string + { + return match ($this->priority) { + 'high' => 'Hoch', + 'low' => 'Niedrig', + default => 'Mittel', + }; + } + + public function statusLabel(): string + { + return match ($this->status) { + 'in_progress' => 'In Bearbeitung', + 'done' => 'Erledigt', + default => 'Offen', + }; + } +} diff --git a/src/app/Models/Translation.php b/src/app/Models/Translation.php new file mode 100644 index 0000000..9f3e539 --- /dev/null +++ b/src/app/Models/Translation.php @@ -0,0 +1,48 @@ +where('locale', $locale); + } + + public function getKeyNameAttribute() + { + return $this->key; + } + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + + // πŸ”₯ CACHE INVALIDIEREN (WICHTIG!) + static::saved(function ($model) { + cache()->forget("translations:{$model->locale}"); + }); + + static::deleted(function ($model) { + cache()->forget("translations:{$model->locale}"); + }); + } +} diff --git a/src/app/Models/User.php b/src/app/Models/User.php new file mode 100644 index 0000000..3cbb075 --- /dev/null +++ b/src/app/Models/User.php @@ -0,0 +1,340 @@ + UserStatus::class, + 'role' => UserRole::class, + 'meta' => 'array', + 'settings' => 'array', + 'notification_settings' => 'array', + 'email_verified_at' => 'datetime', + ]; + + + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + // RELATIONS + public function tokens() + { + return $this->hasMany(ApiToken::class); + } + + public function devices() + { + return $this->hasMany(Device::class); + } + + public function reminders() + { + return $this->hasMany(Reminder::class); + } + + public function activities() + { + return $this->hasMany(Activity::class); + } + + public function contacts() + { + return $this->hasMany(Contact::class); + } + + public function subscriptions() + { + return $this->hasMany(Subscription::class); + } + + public function subscription() + { + // Aktive oder geschenkte Subscription bevorzugen; bei mehreren die neueste nach created_at + return $this->hasOne(Subscription::class) + ->ofMany([ + 'created_at' => 'max', + ], function ($query) { + $query->whereIn('status', [ + \App\Enums\SubscriptionStatus::Active->value, + \App\Enums\SubscriptionStatus::Gifted->value, + ]); + }); + } + + public function latestSubscription() + { + return $this->hasOne(Subscription::class)->latestOfMany('created_at'); + } + + public function agentLogs() + { + return $this->hasMany(\App\Models\AgentLog::class); + } + + public function events() + { + return $this->hasMany(\App\Models\Event::class); + } + + public function automations() + { + return $this->hasMany(Automation::class); + } + + public function calendarIntegrations() + { + return $this->hasMany(CalendarIntegration::class); + } + + public function calendarIntegration(string $provider): ?CalendarIntegration + { + return $this->calendarIntegrations()->where('provider', $provider)->first(); + } + + // ── Credits ─────────────────────────────────────────────────────── + + public function creditTransactions(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(CreditTransaction::class); + } + + /** + * VerfΓΌgbares Guthaben (alle Einzahlungen minus usage-Abbuchungen) + * Der monatliche Reset bucht Über-Plan-Verbrauch als negative usage-Transaktionen. + */ + public function getBonusCreditsAttribute(): int + { + return max(0, (int) $this->creditTransactions()->sum('amount')); + } + + public function getCreditBalanceAttribute(): int + { + return $this->bonus_credits; + } + + public function getMonthlyUsageAttribute(): int + { + return (int) $this->agentLogs() + ->where('created_at', '>=', now()->startOfMonth()) + ->sum('credits'); + } + + /** + * Effektives Limit = Plan-Limit + verfΓΌgbares Guthaben. + * 0 = unlimited (Developer/Admin/Super Admin). + */ + public function getEffectiveLimitAttribute(): int + { + $planLimit = $this->subscription?->plan?->credit_limit ?? 0; + if ($planLimit === 0) return 0; + + return $planLimit + $this->bonus_credits; + } + + public function getEffectiveUsageAttribute(): int + { + return $this->monthly_usage; + } + + public function getUsagePercentAttribute(): int + { + $limit = $this->effective_limit; + if ($limit === 0) return 0; + + return min(100, (int) round(($this->effective_usage / $limit) * 100)); + } + + // Rollen-PrΓΌfungen + + public function hasRole(string|UserRole $role): bool + { + $role = $role instanceof UserRole ? $role : UserRole::from($role); + return $this->role === $role; + } + + public function hasAnyRole(array $roles): bool + { + foreach ($roles as $role) { + $r = $role instanceof UserRole ? $role : UserRole::from($role); + if ($this->role === $r) return true; + } + return false; + } + + public function hasAtLeastRole(string|UserRole $role): bool + { + $role = $role instanceof UserRole ? $role : UserRole::from($role); + return $this->role->hierarchy() >= $role->hierarchy(); + } + + public function isAdmin(): bool + { + return $this->hasAnyRole([UserRole::Admin, UserRole::SuperAdmin]); + } + + public function isSuperAdmin(): bool + { + return $this->role === UserRole::SuperAdmin; + } + + public function isSupport(): bool + { + return $this->hasAtLeastRole(UserRole::Support); + } + + public function isDeveloper(): bool + { + return $this->hasAtLeastRole(UserRole::Developer); + } + + public function isUnlimitedUser(): bool + { + return $this->hasAnyRole([UserRole::Admin, UserRole::SuperAdmin, UserRole::Developer]); + } + + public function isInternalUser(): bool + { + return $this->hasAnyRole([ + UserRole::Admin, UserRole::SuperAdmin, + UserRole::Developer, UserRole::Support, + ]); + } + + public function isBetaTester(): bool + { + return $this->hasAnyRole([ + UserRole::BetaTester, UserRole::Support, + UserRole::Developer, UserRole::Admin, UserRole::SuperAdmin, + ]); + } + + public function hasFeature(string $key): bool + { + // Interne User haben alle Features + if ($this->isInternalUser()) return true; + + $sub = $this->subscription; + if (!$sub || !$sub->isActive()) return false; + return $sub->plan?->features()->where('key', $key)->exists() ?? false; + } + + public function eventsShared() + { + return $this->belongsToMany(Event::class, 'event_user'); + } + + public function notes() + { + return $this->hasMany(Note::class); + } + + public function tasks() + { + return $this->hasMany(Task::class); + } + + public function verifications() + { + return $this->hasMany(Verification::class); + } + + public function emailLogs() + { + return $this->hasMany(EmailLog::class); + } + + public function getSmtpSettingsAttribute(): array + { + $s = $this->settings ?? []; + return [ + 'host' => $s['smtp_host'] ?? null, + 'port' => $s['smtp_port'] ?? 587, + 'encryption' => $s['smtp_encryption'] ?? 'tls', + 'username' => $s['smtp_user'] ?? null, + 'password' => $s['smtp_password'] ?? null, + 'from_address' => $s['smtp_from_address'] ?? null, + 'from_name' => $s['smtp_from_name'] ?? null, + ]; + } + + public function affiliate() + { + return $this->hasOne(Affiliate::class); + } + + public function referredBy() + { + return $this->hasOne(AffiliateReferral::class, 'referred_user_id'); + } + + public function requestDeletion() + { + $this->update([ + 'deletion_requested_at' => now(), + ]); + + $this->delete(); + } + + public function anonymize() + { + $this->update([ + 'name' => 'Deleted User', + 'email' => 'deleted_'.$this->id.'@example.com', + 'password' => null, + 'timezone' => null, + 'locale' => null, + 'settings' => null, + ]); + } +} diff --git a/src/app/Models/Verification.php b/src/app/Models/Verification.php new file mode 100644 index 0000000..63a609e --- /dev/null +++ b/src/app/Models/Verification.php @@ -0,0 +1,54 @@ + 'datetime', + 'verified_at' => 'datetime', + 'resends_reset_at' => 'datetime', + ]; + + protected static function booted() + { + static::creating(function ($model) { + if (!$model->id) { + $model->id = (string) Str::uuid(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function isExpired(): bool + { + return $this->expires_at && now()->gt($this->expires_at); + } + + public function isVerified(): bool + { + return !is_null($this->verified_at); + } + +} diff --git a/src/app/Notifications/EventReminderNotification.php b/src/app/Notifications/EventReminderNotification.php new file mode 100644 index 0000000..9a36d9f --- /dev/null +++ b/src/app/Notifications/EventReminderNotification.php @@ -0,0 +1,35 @@ +event, $this->reminder); + + return [ + 'type' => 'event_reminder', + 'title' => 'Erinnerung: ' . $this->event->title, + 'message' => $job->buildMessage(), + 'event_id' => $this->event->id, + ]; + } +} diff --git a/src/app/Notifications/TaskReminderNotification.php b/src/app/Notifications/TaskReminderNotification.php new file mode 100644 index 0000000..7b05b90 --- /dev/null +++ b/src/app/Notifications/TaskReminderNotification.php @@ -0,0 +1,31 @@ + 'task_reminder', + 'title' => 'Erinnerung: ' . $this->task->title, + 'message' => 'Aufgabe erledigen', + 'task_id' => $this->task->id, + ]; + } +} diff --git a/src/app/Observers/EventObserver.php b/src/app/Observers/EventObserver.php new file mode 100644 index 0000000..709cb8a --- /dev/null +++ b/src/app/Observers/EventObserver.php @@ -0,0 +1,72 @@ +push($event); + } + + public function updated(Event $event): void + { + $this->push($event); + } + + public function deleted(Event $event): void + { + // Kein Push wenn Import lΓ€uft + if (GoogleCalendarService::$syncing) return; + + if (!$event->google_event_id) return; + + $integration = $this->getWritableIntegration($event->user_id); + if (!$integration) return; + + app(GoogleCalendarService::class)->deleteEvent($event->google_event_id, $integration); + + Log::info('Google Calendar: Event gelΓΆscht', [ + 'event_id' => $event->id, + 'google_event_id' => $event->google_event_id, + ]); + } + + // ── Intern ──────────────────────────────────────────────────────────── + + private function push(Event $event): void + { + // Kein Push wenn wir gerade von Google importieren (verhindert Schleife) + if (GoogleCalendarService::$syncing) return; + + $integration = $this->getWritableIntegration($event->user_id); + if (!$integration) return; + + app(GoogleCalendarService::class)->pushEvent($event, $integration); + + Log::info('Google Calendar: Event gepusht', [ + 'event_id' => $event->id, + 'google_event_id' => $event->google_event_id, + ]); + } + + private function getWritableIntegration(string $userId): ?CalendarIntegration + { + return CalendarIntegration::where('user_id', $userId) + ->where('provider', 'google') + ->whereIn('sync_mode', ['write', 'both']) + ->first(); + } +} diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..fb28474 --- /dev/null +++ b/src/app/Providers/AppServiceProvider.php @@ -0,0 +1,51 @@ +environment('production')) { + URL::forceScheme('https'); + } + + FacadesEvent::listen(Registered::class, GiveBonusCredits::class); + + Event::observe(EventObserver::class); + + Blade::component('emails.components.code', 'mail.code'); + Blade::component('emails.components.button', 'mail.button'); + Blade::component('layouts.blank', 'layouts.blank'); + + // Rollen-basierte Gates + Gate::define('access-admin', fn($user) => $user->hasAtLeastRole('support')); + Gate::define('manage-users', fn($user) => $user->isAdmin()); + Gate::define('give-credits', fn($user) => $user->isAdmin()); + Gate::define('gift-access', fn($user) => $user->isAdmin()); + Gate::define('view-technical-logs', fn($user) => $user->isDeveloper()); + Gate::define('change-roles', fn($user) => $user->isSuperAdmin()); + Gate::define('manage-affiliates', fn($user) => $user->isAdmin()); + } +} diff --git a/src/app/Services/AgentAIService.php b/src/app/Services/AgentAIService.php new file mode 100644 index 0000000..b0ab090 --- /dev/null +++ b/src/app/Services/AgentAIService.php @@ -0,0 +1,678 @@ + 'Bearer ' . config('services.openai.key'), + ])->post('https://api.openai.com/v1/chat/completions', [ + 'model' => $model['model'] ?? config('services.openai.model'), + 'messages' => [ + [ + 'role' => 'system', + 'content' => self::systemPrompt() . "\n\nHeutiges Datum: " . now()->format('Y-m-d'), + ], + [ + 'role' => 'user', + 'content' => $input, + ], + ], + 'temperature' => $model['temperature'] ?? 0.2, + 'max_tokens' => $model['max_tokens'] ?? 1000, + ]); + + return array_merge( + self::parseJson($response['choices'][0]['message']['content'] ?? null), + [ + '_usage' => $response['usage'] ?? [], + ] + ); + } + + public static function chat(array $conversationHistory, array $model, string $userContext = ''): array + { + $systemContent = self::chatSystemPrompt(); + + if ($userContext) { + $systemContent .= "\n\n--- KALENDER & DATEN DES BENUTZERS ---\n" . $userContext; + } + + $messages = [['role' => 'system', 'content' => $systemContent]]; + + // Letzte 10 Messages behalten (5 Paare) damit Context nicht zu groß wird + $history = collect($conversationHistory) + ->take(-10) + ->values() + ->map(fn ($msg) => [ + 'role' => $msg['role'], + 'content' => $msg['content'], + ]) + ->toArray(); + + $messages = array_merge($messages, $history); + + $openaiModel = $model['model'] ?? config('services.openai.model'); + + \Log::info('AgentAI: Sending to OpenAI', [ + 'model' => $openaiModel, + 'messages_count' => count($messages), + 'history_count' => count($history), + 'has_key' => !empty(config('services.openai.key')), + ]); + + try { + $response = Http::timeout(45)->withHeaders([ + 'Authorization' => 'Bearer ' . config('services.openai.key'), + ])->post('https://api.openai.com/v1/chat/completions', [ + 'model' => $openaiModel, + 'messages' => $messages, + 'temperature' => $model['temperature'] ?? 0.5, + 'max_tokens' => $model['max_tokens'] ?? 1500, + ]); + + if ($response->failed()) { + \Log::error('AgentAI: OpenAI returned error', [ + 'status' => $response->status(), + 'body' => mb_substr($response->body(), 0, 500), + ]); + return [ + 'type' => 'chat', + 'data' => ['message' => 'Entschuldigung, ich konnte gerade nicht antworten. Bitte versuche es nochmal.'], + '_usage' => [], + '_error' => true, + ]; + } + } catch (\Throwable $e) { + \Log::error('AgentAI: Exception', [ + 'error' => $e->getMessage(), + 'class' => get_class($e), + ]); + report($e); + return [ + 'type' => 'chat', + 'data' => ['message' => 'Entschuldigung, ich konnte gerade keine Verbindung herstellen. Versuch es bitte nochmal.'], + '_usage' => [], + '_error' => true, + ]; + } + + $content = $response['choices'][0]['message']['content'] ?? null; + $usage = $response['usage'] ?? []; + + $parsed = self::parseJson($content); + + // Multi-Action: Array von Aktionen + if (isset($parsed[0]) && is_array($parsed[0])) { + return array_merge(['_multi' => $parsed], ['_usage' => $usage]); + } + + if (($parsed['type'] ?? 'unknown') === 'unknown' && $content) { + // Harte Sicherung: wenn das Modell rohen JSON-Text liefert, der + // NICHT geparst werden konnte, darf der nie als "message" landen β€” + // sonst liest die TTS geschweifte Klammern vor. + $messageText = self::looksLikeJson($content) + ? 'Erledigt!' + : $content; + + if (self::looksLikeJson($content)) { + \Log::warning('AgentAI: JSON-like content in chat fallback β€” replaced', [ + 'preview' => mb_substr($content, 0, 200), + ]); + } + + $parsed = [ + 'type' => 'chat', + 'data' => ['message' => $messageText], + ]; + } + + // Letzter Riegel: jede message-Property bereinigen + if (isset($parsed['data']['message']) && self::looksLikeJson($parsed['data']['message'])) { + \Log::warning('AgentAI: JSON leaked into data.message β€” replaced', [ + 'preview' => mb_substr((string) $parsed['data']['message'], 0, 200), + ]); + $parsed['data']['message'] = 'Erledigt!'; + } + + return array_merge($parsed, ['_usage' => $usage]); + } + + + protected static function systemPrompt(): string + { + return <<withHeaders([ + 'Authorization' => 'Bearer ' . config('services.openai.key'), + ])->post('https://api.openai.com/v1/audio/speech', [ + 'model' => 'tts-1', + 'voice' => $model['tts_voice'] ?? 'nova', + 'input' => $text, + 'response_format' => 'mp3', + 'speed' => $model['tts_speed'] ?? 1.0, + ]); + + if ($response->successful()) { + return base64_encode($response->body()); + } + } catch (\Throwable $e) { + // Timeout oder Netzwerkfehler β†’ null zurΓΌckgeben, Frontend nutzt Browser-TTS + report($e); + } + + return null; + } + + protected static function numbersToWords(string $text): string + { + $ones = ['null','eins','zwei','drei','vier','fΓΌnf','sechs','sieben','acht','neun', + 'zehn','elf','zwΓΆlf','dreizehn','vierzehn','fΓΌnfzehn','sechzehn','siebzehn','achtzehn','neunzehn']; + $tens = ['','','zwanzig','dreißig','vierzig','fΓΌnfzig']; + + $numberToWord = function(int $n) use ($ones, $tens): string { + if ($n >= 0 && $n <= 19) return $ones[$n]; + if ($n >= 20 && $n <= 59) { + $t = (int)($n / 10); + $o = $n % 10; + if ($o === 0) return $tens[$t]; + return $ones[$o] . 'und' . $tens[$t]; // dreiundzwanzig + } + return (string) $n; + }; + + // "10:30" oder "14:00" β†’ "zehn Uhr dreißig" / "vierzehn Uhr" + $text = preg_replace_callback('/(\d{1,2}):(\d{2})/', function($m) use ($numberToWord) { + $h = (int)$m[1]; + $min = (int)$m[2]; + $result = $numberToWord($h) . ' Uhr'; + if ($min > 0) $result .= ' ' . $numberToWord($min); + return $result; + }, $text); + + // "10 Uhr" β†’ "zehn Uhr" (falls AI es als Zahl schreibt) + $text = preg_replace_callback('/(\d{1,2})\s*Uhr/i', function($m) use ($numberToWord) { + return $numberToWord((int)$m[1]) . ' Uhr'; + }, $text); + + // Datum-Zahlen: "14.04." β†’ "vierzehnter vierter" + $text = preg_replace_callback('/(\d{1,2})\.(\d{1,2})\./', function($m) use ($numberToWord) { + $day = $numberToWord((int)$m[1]) . 'ter'; + $month = $numberToWord((int)$m[2]) . 'ter'; + return "{$day} {$month} "; + }, $text); + + return $text; + } + + protected static function chatSystemPrompt(): string + { + return <<utc()->format('Y-m-d H:i') }} UTC, Timezone: {{ now('Europe/Vienna')->format('H:i') }} Wien): +- "in 30 Minuten" β†’ now + 30 Min UTC +- "in 1 Stunde" β†’ now + 60 Min UTC +- "in 2 Stunden" β†’ now + 120 Min UTC +- "um 15 Uhr" β†’ heute 13:00:00 UTC (Vienna = UTC+2) +- "morgen frΓΌh um 8" β†’ morgen 06:00:00 UTC + +Beispiel β€” "Erinnere mich in 58 Min: WΓ€sche aus Waschmaschine": +{"type": "task", "data": {"title": "WΓ€sche aus Waschmaschine", "priority": "medium", "reminder_at": "{{ now()->utc()->addMinutes(58)->format('Y-m-d H:i:s') }}", "due_at": "{{ now()->utc()->addMinutes(58)->format('Y-m-d H:i:s') }}"}} + +CONTACT (nur name ist Pflicht): +{"type": "contact", "data": {"name": "str", "phone": "str", "email": "str", "type": "privat|arbeit|kunde|sonstiges", "notes": "str"}} + +EVENT_UPDATE (inkl. Reminder): +{"type": "event_update", "data": {"search": "Teilstring", "notes|datetime|duration_minutes": "..."}} +{"type": "event_update", "data": {"search": "Teilstring", "reminders": [{"type": "before", "minutes": 10}]}} + +REMINDER TYPEN: +- Minuten/Stunden vorher: {"type": "before", "minutes": 10} + Beispiele: "10 Minuten vorher" β†’ minutes: 10, "1 Stunde vorher" β†’ minutes: 60 +- Uhrzeit am Tag des Termins: {"type": "time_of_day", "time": "08:00"} +- Am Vortag um Uhrzeit: {"type": "day_before", "time": "18:00"} + +Wenn User sagt "Erinnere mich 10 Min vorher und morgen frΓΌh um 8" fΓΌr Termin: +{"type": "event_update", "data": {"search": "Termintitel", "reminders": [{"type": "before", "minutes": 10}, {"type": "day_before", "time": "08:00"}]}} + +NOTE_UPDATE: +{"type": "note_update", "data": {"search": "Teilstring", "content": "Zusatz"}} + +TASK_UPDATE: +{"type": "task_update", "data": {"search": "Teilstring", "description|status": "...|done"}} + +EMAIL: +{"type": "email", "data": {"contact": "Name", "message": "Text", "subject": "Betreff"}} +{"type": "email", "data": {"contact": "Name", "event": "Termintitel-Teilstring", "message": "opt."}} + +Multi: [{...}, {...}] + +PROMPT; + } + + protected static function parseJson(?string $text): array + { + if (!$text) { + return self::fallback(); + } + + // Markdown-Codeblocks entfernen (```json ... ``` oder ``` ... ```) + $cleaned = trim($text); + if (preg_match('/```(?:json)?\s*([\s\S]*?)```/', $cleaned, $matches)) { + $cleaned = trim($matches[1]); + } + + $json = json_decode($cleaned, true); + + if ($json === null) { + return self::fallback(); + } + + // Array von Aktionen (Multi-Action) + if (isset($json[0]) && is_array($json[0])) { + return $json; // Wird in chat() als _multi erkannt + } + + return $json; + } + + protected static function fallback(): array + { + return [ + 'type' => 'unknown', + 'data' => [], + ]; + } + + /** + * PrΓΌft, ob ein String wie rohes JSON aussieht und somit NIEMALS + * vorgelesen werden darf. + */ + protected static function looksLikeJson(?string $text): bool + { + if (!$text) return false; + $trim = ltrim($text); + if ($trim === '') return false; + $first = $trim[0]; + if ($first === '{' || $first === '[') return true; + if (str_contains($text, '"type":')) return true; + if (str_contains($text, '"data":')) return true; + return false; + } +} diff --git a/src/app/Services/AgentActionService.php b/src/app/Services/AgentActionService.php new file mode 100644 index 0000000..192e007 --- /dev/null +++ b/src/app/Services/AgentActionService.php @@ -0,0 +1,732 @@ + self::handleEvent($user, $parsed['data']), + 'event_update' => self::handleEventUpdate($user, $parsed['data']), + 'note' => self::handleNote($user, $parsed['data']), + 'note_update' => self::handleNoteUpdate($user, $parsed['data']), + 'task' => self::handleTask($user, $parsed['data']), + 'task_update' => self::handleTaskUpdate($user, $parsed['data']), + 'contact' => self::handleContact($user, $parsed['data']), + 'email' => self::handleEmail($user, $parsed['data']), + default => self::handleUnknown($parsed), + }; + } + + + protected static function handleEvent(User $user, array $data): array + { + $planner = app(EventPlannerService::class); + + /* + |-------------------------------------------------------------------------- + | πŸ”₯ 1. ZEITRAUM (AI hat start + end geliefert) + |-------------------------------------------------------------------------- + */ + + if (!empty($data['start']) && empty($data['end']) && ($data['is_all_day'] ?? false)) { + + if (!empty($data['range_end'])) { + $data['end'] = $data['range_end']; + } + } + + if (!empty($data['start']) && !empty($data['end'])) { + + return $planner->plan($user, array_merge($data, [ + 'title' => $data['title'] ?? 'Termin', + 'start' => $data['start'], + 'end' => $data['end'], + 'duration_minutes' => Carbon::parse($data['start']) + ->diffInMinutes(Carbon::parse($data['end'])), + 'is_all_day' => $data['is_all_day'] ?? false, + 'exceptions' => $data['exceptions'] ?? null, + ])); + } + + /* + |-------------------------------------------------------------------------- + | πŸ”₯ 2. NORMALER TERMIN (datetime) + |-------------------------------------------------------------------------- + */ + + if (!empty($data['datetime'])) { + + return $planner->plan($user, array_merge($data, [ + 'title' => $data['title'] ?? 'Termin', + 'start' => $data['datetime'], + 'duration_minutes' => $data['duration_minutes'] ?? null, + 'ai_duration' => $data['ai_duration'] ?? null, + ])); + } + + /* + |-------------------------------------------------------------------------- + | ❌ FALLBACK + |-------------------------------------------------------------------------- + */ + + return [ + 'status' => 'failed', + 'message' => 'Keine Zeit erkannt', + 'meta' => [] + ]; + } + + + protected static function handleNote(User $user, array $data): array + { + $note = Note::create([ + 'user_id' => $user->id, + 'title' => $data['title'] ?? null, + 'content' => $data['content'] ?? $data['text'] ?? '', + 'color' => $data['color'] ?? 'yellow', + ]); + + Activity::log( + $user->id, + Activity::TYPE_NOTE_CREATED, + Activity::localizedTitle('note_created_assistant', $user->locale ?? 'de'), + $note->title, + ); + + // Verifikation + $verified = Note::find($note->id); + if (! $verified) { + \Log::error('Action verify failed: note not found after create', ['note_id' => $note->id]); + return ['status' => 'error', 'message' => 'Notiz konnte nicht erstellt werden. Bitte nochmal versuchen.', 'meta' => []]; + } + + \Log::info('Action verified', ['type' => 'note', 'note_id' => $verified->id, 'verified' => true]); + + return [ + 'status' => 'success', + 'verified' => true, + 'message' => "Notiert! \"{$verified->title}\"", + 'meta' => ['title' => $verified->title, 'content' => $verified->content], + ]; + } + + protected static function handleTask(User $user, array $data): array + { + $title = $data['title'] ?? $data['description'] ?? 'Aufgabe'; + + // due_at: AI schickt due_at, due_date oder datetime + $dueAt = null; + if (!empty($data['due_at'])) { + try { $dueAt = Carbon::parse($data['due_at'])->utc(); } catch (\Throwable) {} + } elseif (!empty($data['due_date'])) { + try { $dueAt = Carbon::parse($data['due_date'], $user->timezone); } catch (\Throwable) {} + } elseif (!empty($data['datetime'])) { + try { $dueAt = Carbon::parse($data['datetime'], $user->timezone)->startOfDay(); } catch (\Throwable) {} + } + + // reminder_at: direkt aus AI-Response (UTC) + $reminderAt = null; + if (!empty($data['reminder_at'])) { + try { $reminderAt = Carbon::parse($data['reminder_at'])->utc(); } catch (\Throwable) {} + } + + $task = Task::create([ + 'user_id' => $user->id, + 'title' => $title, + 'description' => $data['description'] ?? null, + 'priority' => $data['priority'] ?? Task::PRIORITY_MEDIUM, + 'status' => Task::STATUS_PENDING, + 'due_at' => $dueAt, + 'reminder_at' => $reminderAt, + 'reminder_sent' => false, + ]); + + Activity::log( + $user->id, + Activity::TYPE_TASK_CREATED, + Activity::localizedTitle('task_created_assistant', $user->locale ?? 'de'), + $task->title, + meta: ['priority' => $task->priority] + ); + + // Verifikation + $verified = Task::find($task->id); + if (! $verified) { + \Log::error('Action verify failed: task not found after create', ['task_id' => $task->id]); + return ['status' => 'error', 'message' => 'Aufgabe konnte nicht erstellt werden. Bitte nochmal versuchen.', 'meta' => []]; + } + + \Log::info('Action verified', ['type' => 'task', 'task_id' => $verified->id, 'verified' => true]); + + $tz = $user->timezone ?? 'Europe/Vienna'; + $dueText = $dueAt ? ' (fΓ€llig: ' . $dueAt->setTimezone($tz)->format('d.m.Y') . ')' : ''; + $reminderText = $reminderAt ? ' Erinnerung: ' . $reminderAt->setTimezone($tz)->format('H:i') . ' Uhr.' : ''; + + return [ + 'status' => 'success', + 'verified' => true, + 'message' => "Erledigt! Aufgabe \"{$verified->title}\" erstellt{$dueText}.{$reminderText}", + 'meta' => [ + 'title' => $verified->title, + 'priority' => $verified->priorityLabel(), + 'due_at' => $dueAt?->setTimezone($tz)->format('d.m.Y'), + 'reminder_at' => $reminderAt?->setTimezone($tz)->format('H:i'), + ], + ]; + } + + protected static function handleEventUpdate(User $user, array $data): array + { + $search = $data['search'] ?? ''; + + if (!$search) { + return ['status' => 'failed', 'message' => 'Kein Termin angegeben', 'meta' => []]; + } + + $tz = $user->timezone ?? 'Europe/Vienna'; + + // Diagnose-Log: wie viele Kandidaten existieren, welcher wird gewΓ€hlt? + $candidates = Event::where('user_id', $user->id) + ->where('title', 'like', '%' . $search . '%') + ->where('starts_at', '>=', now()->subDays(7)) + ->orderBy('starts_at') + ->get(['id', 'title', 'starts_at']); + + \Log::info('EventUpdate: Kandidatensuche', [ + 'user_id' => $user->id, + 'search' => $search, + 'candidate_count' => $candidates->count(), + 'candidates' => $candidates->map(fn($c) => [ + 'id' => $c->id, + 'title' => $c->title, + 'starts_at' => $c->starts_at?->toIso8601String(), + ])->all(), + 'incoming_data' => $data, + ]); + + $event = $candidates->first() ? Event::find($candidates->first()->id) : null; + + if (!$event) { + return ['status' => 'failed', 'message' => "Kein Termin mit \"{$search}\" gefunden", 'meta' => ['search' => $search]]; + } + + \Log::info('EventUpdate: AusgewΓ€hlter Termin (vor Γ„nderung)', [ + 'event_id' => $event->id, + 'title' => $event->title, + 'starts_at_utc' => $event->starts_at?->toIso8601String(), + 'starts_at_local' => $event->starts_at?->copy()->setTimezone($tz)->toIso8601String(), + 'ends_at_utc' => $event->ends_at?->toIso8601String(), + ]); + + $changes = []; + $expectedStart = null; + + // Notiz hinzufΓΌgen + if (!empty($data['notes'])) { + $event->notes = $event->notes + ? $event->notes . "\n" . $data['notes'] + : $data['notes']; + $changes[] = 'Notiz hinzugefΓΌgt'; + } + + // Verschieben (neues Datum/Uhrzeit) + if (!empty($data['datetime'])) { + $newStart = Carbon::parse($data['datetime'], $tz)->utc(); + $duration = $event->ends_at ? $event->starts_at->diffInMinutes($event->ends_at) : 60; + $event->starts_at = $newStart; + $event->ends_at = $newStart->copy()->addMinutes($duration); + $expectedStart = $newStart; + $changes[] = 'verschoben'; + } + + // Nur Startzeit Γ€ndern + if (!empty($data['start'])) { + $newStart = Carbon::parse($data['start'], $tz)->utc(); + $duration = $event->ends_at ? $event->starts_at->diffInMinutes($event->ends_at) : 60; + $event->starts_at = $newStart; + $event->ends_at = $newStart->copy()->addMinutes($duration); + $expectedStart = $newStart; + $changes[] = 'Start geΓ€ndert'; + } + + // Endzeit / Dauer Γ€ndern + if (!empty($data['end'])) { + $event->ends_at = Carbon::parse($data['end'], $tz)->utc(); + $changes[] = 'Ende geΓ€ndert'; + } + if (!empty($data['duration_minutes'])) { + $event->ends_at = $event->starts_at->copy()->addMinutes((int) $data['duration_minutes']); + $changes[] = 'Dauer geΓ€ndert'; + } + + // Titel Γ€ndern + if (!empty($data['title'])) { + $event->title = $data['title']; + $changes[] = 'Titel geΓ€ndert'; + } + + // Reminder setzen + if (isset($data['reminders']) && is_array($data['reminders'])) { + $event->reminders = $data['reminders']; + $count = count($data['reminders']); + $changes[] = $count === 1 ? '1 Erinnerung gesetzt' : "{$count} Erinnerungen gesetzt"; + } + + \Log::info('EventUpdate: vor save()', [ + 'event_id' => $event->id, + 'expected_start_utc' => $expectedStart?->toIso8601String(), + 'expected_start_local' => $expectedStart?->copy()->setTimezone($tz)->toIso8601String(), + 'new_starts_at_utc' => $event->starts_at?->toIso8601String(), + 'changes' => $changes, + ]); + + $event->save(); + + // ── Verifikation ── + $verified = Event::find($event->id); + + \Log::info('EventUpdate: nach save() (DB-Stand)', [ + 'event_id' => $verified?->id, + 'actual_start_utc' => $verified?->starts_at?->toIso8601String(), + 'actual_start_local' => $verified?->starts_at?->copy()->setTimezone($tz)->toIso8601String(), + ]); + + if (! $verified) { + \Log::error('Action verify failed: event not found after save', ['event_id' => $event->id]); + return ['status' => 'error', 'message' => 'Der Termin wurde nicht gespeichert. Bitte nochmal versuchen.', 'verified' => false, 'meta' => []]; + } + + if ($expectedStart) { + $verifiedStart = $verified->starts_at->setTimezone($tz); + $expectedDate = $expectedStart->copy()->setTimezone($tz)->format('Y-m-d'); + $actualDate = $verifiedStart->format('Y-m-d'); + + if ($expectedDate !== $actualDate) { + // Nochmals direkt updaten + $verified->update([ + 'starts_at' => $expectedStart, + 'ends_at' => $expectedStart->copy()->addMinutes( + $verified->ends_at ? $verified->starts_at->diffInMinutes($verified->ends_at) : 60 + ), + ]); + $verified->refresh(); + $verifiedStart = $verified->starts_at->setTimezone($tz); + + if ($verifiedStart->format('Y-m-d') !== $expectedDate) { + \Log::error('Action verify failed: event date mismatch after retry', [ + 'event_id' => $event->id, + 'expected' => $expectedDate, + 'actual' => $verifiedStart->format('Y-m-d'), + ]); + return ['status' => 'error', 'message' => 'Das Verschieben hat nicht funktioniert. Bitte nochmal versuchen.', 'verified' => false, 'meta' => []]; + } + } + + \Log::info('Action verified', [ + 'type' => 'event_update', + 'event_id' => $verified->id, + 'verified' => true, + 'date' => $verifiedStart->format('d.m.Y H:i'), + ]); + + return [ + 'status' => 'success', + 'verified' => true, + 'message' => "Erledigt! {$verified->title} ist jetzt am {$verifiedStart->format('d.m.Y')} um {$verifiedStart->format('H:i')} Uhr.", + 'meta' => ['title' => $verified->title, 'notes' => $verified->notes], + ]; + } + + $summary = implode(', ', $changes) ?: 'aktualisiert'; + + \Log::info('Action verified', [ + 'type' => 'event_update', + 'event_id' => $verified->id, + 'verified' => true, + 'changes' => $summary, + ]); + + return [ + 'status' => 'success', + 'verified' => true, + 'message' => "Erledigt! \"{$verified->title}\" wurde {$summary}.", + 'meta' => ['title' => $verified->title, 'notes' => $verified->notes], + ]; + } + + protected static function handleNoteUpdate(User $user, array $data): array + { + $search = $data['search'] ?? ''; + + if (!$search) { + return ['status' => 'failed', 'message' => 'Keine Notiz angegeben', 'meta' => []]; + } + + $note = Note::where('user_id', $user->id) + ->where(function ($q) use ($search) { + $q->where('title', 'like', '%' . $search . '%') + ->orWhere('content', 'like', '%' . $search . '%'); + }) + ->latest() + ->first(); + + if (!$note) { + return ['status' => 'failed', 'message' => "Keine Notiz mit \"{$search}\" gefunden", 'meta' => ['search' => $search]]; + } + + if (!empty($data['content'])) { + $note->content = $note->content + ? $note->content . "\n" . $data['content'] + : $data['content']; + } + + if (!empty($data['title'])) { + $note->title = $data['title']; + } + + $note->save(); + + // Verifikation + $verified = Note::find($note->id); + if (! $verified) { + \Log::error('Action verify failed: note not found after update', ['note_id' => $note->id]); + return ['status' => 'error', 'message' => 'Notiz konnte nicht aktualisiert werden.', 'meta' => []]; + } + + \Log::info('Action verified', ['type' => 'note_update', 'note_id' => $verified->id, 'verified' => true]); + + return [ + 'status' => 'success', + 'verified' => true, + 'message' => "Erledigt! Notiz \"{$verified->title}\" wurde aktualisiert.", + 'meta' => ['title' => $verified->title, 'content' => $verified->content], + ]; + } + + protected static function handleTaskUpdate(User $user, array $data): array + { + $search = $data['search'] ?? ''; + + if (!$search) { + return ['status' => 'failed', 'message' => 'Keine Aufgabe angegeben', 'meta' => []]; + } + + $task = Task::where('user_id', $user->id) + ->where('title', 'like', '%' . $search . '%') + ->whereNull('completed_at') + ->latest() + ->first(); + + if (!$task) { + return ['status' => 'failed', 'message' => "Keine Aufgabe mit \"{$search}\" gefunden", 'meta' => ['search' => $search]]; + } + + if (!empty($data['title'])) { + $task->title = $data['title']; + } + + if (!empty($data['description'])) { + $task->description = $task->description + ? $task->description . "\n" . $data['description'] + : $data['description']; + } + + if (!empty($data['status'])) { + $task->status = $data['status']; + if ($data['status'] === 'done') { + $task->completed_at = now(); + } + } + + if (!empty($data['priority'])) { + $task->priority = $data['priority']; + } + + $expectedStatus = $data['status'] ?? null; + $task->save(); + + // Verifikation + $verified = Task::find($task->id); + if (! $verified) { + \Log::error('Action verify failed: task not found after update', ['task_id' => $task->id]); + return ['status' => 'error', 'message' => 'Aufgabe konnte nicht aktualisiert werden.', 'meta' => []]; + } + + if ($expectedStatus && $verified->status !== $expectedStatus) { + \Log::error('Action verify failed: task status mismatch', [ + 'task_id' => $task->id, + 'expected' => $expectedStatus, + 'actual' => $verified->status, + ]); + return ['status' => 'error', 'message' => 'Aufgabe konnte nicht aktualisiert werden.', 'meta' => []]; + } + + $statusText = $expectedStatus === 'done' ? 'als erledigt markiert' : 'aktualisiert'; + + \Log::info('Action verified', ['type' => 'task_update', 'task_id' => $verified->id, 'verified' => true]); + + return [ + 'status' => 'success', + 'verified' => true, + 'message' => "Erledigt! \"{$verified->title}\" wurde {$statusText}.", + 'meta' => ['title' => $verified->title], + ]; + } + + protected static function handleContact(User $user, array $data): array + { + $name = $data['name'] ?? null; + + if (!$name) { + return ['status' => 'failed', 'message' => 'Kein Name angegeben', 'meta' => []]; + } + + $type = $data['type'] ?? 'sonstiges'; + if (!in_array($type, Contact::TYPES)) { + $type = 'sonstiges'; + } + + $contact = Contact::create([ + 'user_id' => $user->id, + 'name' => $name, + 'email' => $data['email'] ?? null, + 'phone' => $data['phone'] ?? null, + 'type' => $type, + 'notes' => $data['notes'] ?? null, + ]); + + Activity::log( + $user->id, + Activity::TYPE_CONTACT_CREATED, + Activity::localizedTitle('contact_created_assistant', $user->locale ?? 'de'), + $contact->name, + ); + + // Verifikation + $verified = Contact::find($contact->id); + if (! $verified) { + \Log::error('Action verify failed: contact not found after create', ['contact_id' => $contact->id]); + return ['status' => 'error', 'message' => 'Kontakt konnte nicht erstellt werden. Bitte nochmal versuchen.', 'meta' => []]; + } + + \Log::info('Action verified', ['type' => 'contact', 'contact_id' => $verified->id, 'verified' => true]); + + return [ + 'status' => 'success', + 'verified' => true, + 'message' => "Erledigt! Kontakt \"{$verified->name}\" wurde angelegt.", + 'meta' => [ + 'title' => $verified->name, + 'type' => $verified->typeLabel(), + 'phone' => $verified->phone, + 'email' => $verified->email, + ], + ]; + } + + /** + * Kontakt suchen (fuzzy). + * Gibt zurΓΌck: Contact | null | array von Contacts (bei Mehrdeutigkeit) + */ + protected static function findContact(User $user, string $search): Contact|array|null + { + if (!$search) return null; + + $search = trim($search); + $allContacts = Contact::where('user_id', $user->id)->get(); + + if ($allContacts->isEmpty()) return null; + + $searchLower = mb_strtolower($search); + $searchWords = preg_split('/\s+/', $searchLower); + + // Matching-Funktion: sammelt alle passenden Kontakte einer Stufe + $matchMany = function ($allContacts, callable $matcher) { + $matches = $allContacts->filter($matcher)->values(); + return $matches->isEmpty() ? null : $matches; + }; + + // 1. Exakter Wort-Match (Suchwort stimmt mit einem Namens-Teil exakt ΓΌberein) + $matches = $matchMany($allContacts, function ($c) use ($searchWords) { + $nameParts = preg_split('/\s+/', mb_strtolower($c->name)); + foreach ($searchWords as $word) { + if (in_array($word, $nameParts, true)) return true; + } + return false; + }); + if ($matches && $matches->count() === 1) return $matches->first(); + if ($matches && $matches->count() > 1) return $matches->all(); + + // 2. Voller Suchstring als Substring im Namen + $matches = $matchMany($allContacts, fn($c) => + str_contains(mb_strtolower($c->name), $searchLower) + || ($c->email && str_contains(mb_strtolower($c->email), $searchLower)) + ); + if ($matches && $matches->count() === 1) return $matches->first(); + if ($matches && $matches->count() > 1) return $matches->all(); + + // 3. Wort-basiert + $matches = $matchMany($allContacts, function ($c) use ($searchWords) { + $name = mb_strtolower($c->name); + foreach ($searchWords as $word) { + if (mb_strlen($word) >= 2 && str_contains($name, $word)) return true; + } + return false; + }); + if ($matches && $matches->count() === 1) return $matches->first(); + if ($matches && $matches->count() > 1) return $matches->all(); + + // 4. Progressiv kΓΌrzen + for ($cut = 1; $cut <= 3; $cut++) { + $shortened = mb_substr($searchLower, 0, -$cut); + if (mb_strlen($shortened) < 2) break; + + $matches = $matchMany($allContacts, fn($c) => + str_contains(mb_strtolower($c->name), $shortened) + ); + if ($matches && $matches->count() === 1) return $matches->first(); + if ($matches && $matches->count() > 1) return $matches->all(); + } + + // 5. SOUNDEX + $matches = $matchMany($allContacts, function ($c) use ($searchWords) { + $nameParts = preg_split('/\s+/', $c->name); + foreach ($searchWords as $word) { + if (mb_strlen($word) < 2) continue; + $sx = soundex($word); + foreach ($nameParts as $part) { + if (soundex($part) === $sx) return true; + } + } + return false; + }); + if ($matches && $matches->count() === 1) return $matches->first(); + if ($matches && $matches->count() > 1) return $matches->all(); + + // 6. Levenshtein + $scored = []; + foreach ($allContacts as $c) { + $nameParts = preg_split('/\s+/', mb_strtolower($c->name)); + foreach ($nameParts as $part) { + foreach ($searchWords as $word) { + $dist = levenshtein($word, $part); + $threshold = max(1, (int) floor(mb_strlen($word) * 0.4)); + if ($dist <= $threshold) { + $scored[$c->id] = ['contact' => $c, 'dist' => $dist]; + } + } + } + } + + if (count($scored) === 1) return array_values($scored)[0]['contact']; + if (count($scored) > 1) return array_map(fn($s) => $s['contact'], array_values($scored)); + + return null; + } + + protected static function handleEmail(User $user, array $data): array + { + $contactSearch = $data['contact'] ?? ''; + $eventSearch = $data['event'] ?? ''; + $subject = $data['subject'] ?? null; + $message = $data['message'] ?? ''; + + // Kontakt suchen (fuzzy) + $result = self::findContact($user, $contactSearch); + + // Mehrdeutig: mehrere Treffer β†’ nachfragen + if (is_array($result)) { + $names = array_map(fn($c) => $c->name, $result); + return [ + 'status' => 'ambiguous', + 'message' => 'Ich habe mehrere Kontakte gefunden: ' . implode(', ', $names) . '. Welchen meinst du?', + 'meta' => ['candidates' => $names], + ]; + } + + $contact = $result; + $toEmail = $contact?->email ?? ($data['email'] ?? null); + $toName = $contact?->name ?? ($data['name'] ?? 'EmpfΓ€nger'); + + if (!$toEmail) { + return [ + 'status' => 'failed', + 'message' => $contact + ? "Kontakt \"{$contact->name}\" hat keine E-Mail-Adresse" + : "Kein Kontakt mit \"{$contactSearch}\" gefunden", + 'meta' => [], + ]; + } + + // Event suchen (optional, fΓΌr Terminerinnerung) + $event = null; + if ($eventSearch) { + $event = Event::where('user_id', $user->id) + ->where('title', 'like', '%' . $eventSearch . '%') + ->where('starts_at', '>=', now()->subDay()) + ->orderBy('starts_at') + ->first(); + } + + $tz = $user->timezone ?? 'UTC'; + + // Meta fΓΌr Template + $meta = [ + 'sender_name' => $user->name, + 'recipient_name' => $toName, + 'message' => $message, + ]; + + if ($event) { + $meta['event_title'] = $event->title; + $meta['event_date'] = $event->starts_at->setTimezone($tz)->format('d.m.Y'); + $meta['event_time'] = $event->starts_at->setTimezone($tz)->format('H:i'); + $meta['event_end'] = $event->ends_at?->setTimezone($tz)->format('H:i'); + $meta['event_notes'] = $event->notes; + $subject = $subject ?? "Terminerinnerung: {$event->title}"; + } + + $template = $event ? 'agent.reminder' : 'agent.message'; + $subject = $subject ?? 'Nachricht von ' . $user->name; + + MailService::queue( + to: $toEmail, + template: $template, + meta: $meta, + subject: $subject, + userId: $user->id, + ); + + return [ + 'status' => 'success', + 'message' => "E-Mail an {$toName} gesendet", + 'meta' => [ + 'title' => "E-Mail an {$toName}", + 'to' => $toEmail, + 'subject' => $subject, + ], + ]; + } + + protected static function handleUnknown(array $parsed): array + { + return [ + 'status' => 'failed', + 'message' => 'Nicht verstanden', + 'meta' => $parsed, + ]; + } +} diff --git a/src/app/Services/AgentContextService.php b/src/app/Services/AgentContextService.php new file mode 100644 index 0000000..03ad85b --- /dev/null +++ b/src/app/Services/AgentContextService.php @@ -0,0 +1,153 @@ +timezone ?? 'Europe/Berlin'; + $now = Carbon::now($tz); + $todayStartLocal = $now->copy()->startOfDay(); + $weekEndLocal = $now->copy()->addDays(7)->endOfDay(); + $todayStartUtc = $todayStartLocal->copy()->utc(); + $weekEndUtc = $weekEndLocal->copy()->utc(); + + $dayNames = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; + + $lines = []; + + // Tagesreferenz (AI ordnet Wochentage korrekt zu) + $lines[] = 'TAGE:'; + for ($i = 0; $i < 7; $i++) { + $day = $now->copy()->addDays($i); + $label = match ($i) { + 0 => ' (heute)', + 1 => ' (morgen)', + default => '', + }; + $lines[] = "- {$dayNames[$day->dayOfWeek]} = {$day->format('d.m.Y')}{$label}"; + } + + // Termine: laufend/kommend innerhalb 7 Tage + $events = Event::where('user_id', $user->id) + ->where(function ($q) use ($todayStartUtc, $weekEndUtc) { + $q->whereBetween('starts_at', [$todayStartUtc, $weekEndUtc]) + ->orWhere(function ($q2) use ($todayStartUtc) { + $q2->where('starts_at', '<', $todayStartUtc) + ->where('ends_at', '>=', $todayStartUtc); + }); + }) + ->orderBy('starts_at') + ->limit(30) + ->get(); + + if ($events->isNotEmpty()) { + $lines[] = "\nTERMINE (heute + 7 Tage):"; + foreach ($events as $e) { + $start = $e->starts_at->copy()->setTimezone($tz); + $end = $e->ends_at?->copy()->setTimezone($tz); + $isMultiDay = $end && $start->toDateString() !== $end->toDateString(); + + if ($isMultiDay) { + $totalDays = $start->copy()->startOfDay()->diffInDays($end->copy()->startOfDay()) + 1; + $cursor = $start->copy()->startOfDay(); + $dayNum = 1; + while ($cursor->lte($end->copy()->startOfDay())) { + if ($cursor->gte($todayStartLocal) && $cursor->lte($weekEndLocal)) { + $dn = $dayNames[$cursor->dayOfWeek] ?? ''; + if ($e->is_all_day) { + $timeInfo = 'GanztΓ€gig'; + } elseif ($dayNum === 1) { + $timeInfo = "ab {$start->format('H:i')}"; + } elseif ($dayNum === $totalDays) { + $timeInfo = "bis {$end->format('H:i')}"; + } else { + $timeInfo = 'ganztΓ€gig'; + } + $lines[] = "- [{$e->id}] {$dn} {$cursor->format('d.m.')} {$timeInfo} {$e->title} (Tag {$dayNum} von {$totalDays})"; + } + $cursor->addDay(); + $dayNum++; + } + } else { + $lines[] = '- ' . $this->formatEvent($e, $tz, $dayNames); + } + } + } else { + $lines[] = "\nTERMINE: Keine Termine."; + } + + // Notizen + $notes = Note::where('user_id', $user->id) + ->latest() + ->limit(5) + ->get(); + + if ($notes->isNotEmpty()) { + $lines[] = "\nNOTIZEN:"; + foreach ($notes as $n) { + $label = $n->title ?: mb_substr($n->content, 0, 50); + $lines[] = "- [{$n->id}] {$label}"; + } + } + + // Offene Aufgaben + $tasks = Task::where('user_id', $user->id) + ->whereNull('completed_at') + ->orderBy('due_at') + ->limit(8) + ->get(); + + if ($tasks->isNotEmpty()) { + $lines[] = "\nOFFENE AUFGABEN:"; + foreach ($tasks as $t) { + $lines[] = "- [{$t->id}] {$t->title}"; + } + } + + // Kontakte + $contacts = Contact::where('user_id', $user->id) + ->orderBy('name') + ->limit(50) + ->get(); + + if ($contacts->isNotEmpty()) { + $lines[] = "\nKONTAKTE:"; + foreach ($contacts as $c) { + $email = $c->email ? " ({$c->email})" : ''; + $lines[] = "- [{$c->id}] {$c->name}{$email}"; + } + } else { + $lines[] = "\nKONTAKTE: Keine Kontakte vorhanden."; + } + + return implode("\n", $lines); + } + + private function formatEvent(Event $e, string $tz, array $dayNames): string + { + $start = $e->starts_at->setTimezone($tz); + $end = $e->ends_at?->setTimezone($tz); + $dayName = $dayNames[$start->dayOfWeek] ?? ''; + + if ($e->is_all_day) { + return "[{$e->id}] {$dayName} {$start->format('d.m.')} GanztΓ€gig {$e->title}"; + } + + $time = $end ? "{$start->format('H:i')}-{$end->format('H:i')}" : $start->format('H:i'); + return "[{$e->id}] {$dayName} {$start->format('d.m.')} {$time} {$e->title}"; + } +} diff --git a/src/app/Services/AgentParserService.php b/src/app/Services/AgentParserService.php new file mode 100644 index 0000000..3f7b1ca --- /dev/null +++ b/src/app/Services/AgentParserService.php @@ -0,0 +1,114 @@ + 'unknown', 'data' => []]; + $aiUsage = $ai['usage'] ?? null; + + if (($ai['type'] ?? 'unknown') !== 'unknown') { + return $ai; + } + + // πŸ”₯ 2. FALLBACK β†’ einfacher Parser + if (self::isEvent($text)) { + return self::parseEvent($text); + } + + if (self::isNote($text)) { + return self::parseNote($text); + } + + // πŸ”₯ 3. GAR NICHTS ERKANNT + return [ + 'type' => 'unknown', + 'data' => [], + ]; + } + + protected static function isEvent(string $text): bool + { + return str_contains($text, 'termin'); + } + + protected static function isNote(string $text): bool + { + return str_contains($text, 'notiz'); + } + + protected static function parseEvent(string $text): array + { + $datetime = self::extractDateTime($text); + + // πŸ”₯ saubere Titel-Extraktion + $title = preg_replace('/termin|morgen|heute|\d{1,2}:\d{2}|\d{1,2}\s*uhr/', '', $text); + $title = ucfirst(trim($title)); + + return [ + 'type' => 'event', + 'data' => [ + 'title' => $title, + 'datetime' => $datetime?->toDateTimeString(), + ], + ]; + } + + protected static function parseNote(string $text): array + { + $content = trim(str_replace('notiz', '', $text)); + + return [ + 'type' => 'note', + 'data' => [ + 'content' => ucfirst($content), + ], + ]; + } + + protected static function extractDateTime(string $text): ?CarbonInterface + { + $date = now(); + + // πŸ”₯ NEU: "in X tagen" + if (preg_match('/in\s+(\d+)\s+tagen/', $text, $matches)) { + $date = now()->addDays((int)$matches[1]); + } + + // πŸ”₯ OPTIONAL + if (str_contains($text, 'ΓΌbermorgen')) { + $date = now()->addDays(2); + } + + if (str_contains($text, 'morgen')) { + $date = now()->addDay(); + } + + if (str_contains($text, 'heute')) { + $date = now(); + } + + // πŸ”₯ 15:00 + if (preg_match('/(\d{1,2}):(\d{2})/', $text, $matches)) { + return $date->copy()->setTime($matches[1], $matches[2]); + } + + // πŸ”₯ 15 uhr + if (preg_match('/(\d{1,2})\s*uhr/', $text, $matches)) { + return $date->copy()->setTime($matches[1], 0); + } + + return null; + } +} diff --git a/src/app/Services/DurationLearningService.php b/src/app/Services/DurationLearningService.php new file mode 100644 index 0000000..860c089 --- /dev/null +++ b/src/app/Services/DurationLearningService.php @@ -0,0 +1,226 @@ +resolveKeyword($title, $userId); + + // πŸ‘‰ Schutz gegen MΓΌll + if ( + !$keyword || + in_array($keyword, ['termin', 'machen', 'gehen']) || + $duration < 5 || + $duration > 600 + ) { + return; + } + + $stat = EventDurationStat::firstOrNew([ + 'user_id' => $userId, + 'keyword' => $keyword, + ]); + + if (!$stat->exists) { + $stat->avg_duration = $duration; + $stat->count = 1; + $stat->save(); + return; + } + + // πŸ‘‰ stabiler Durchschnitt + $stat->avg_duration = round( + ($stat->avg_duration * 0.7) + ($duration * 0.3) + ); + + $stat->count += 1; + $stat->save(); + } + + /* + |-------------------------------------------------------------------------- + | GET + |-------------------------------------------------------------------------- + */ + + public function get(string $userId, string $title): ?int + { + $keyword = $this->resolveKeyword($title, $userId); + if (!$keyword) return null; + + return EventDurationStat::where('user_id', $userId) + ->where('keyword', $keyword) + ->where(function ($q) { + $q->where('count', '>=', 2) + ->orWhere('avg_duration', '<=', 30); + }) + ->value('avg_duration'); + } + + /* + |-------------------------------------------------------------------------- + | KEYWORD RESOLUTION + |-------------------------------------------------------------------------- + */ + + protected function resolveKeyword(string $title, string $userId): ?string + { + $normalized = $this->normalize($title); + + $cacheKey = $userId . ':' . $normalized; + + if (isset($this->keywordCache[$cacheKey])) { + return $this->keywordCache[$cacheKey]; + } + + // πŸ‘‰ DB Lookup + $existing = EventKeyword::where('user_id', $userId) + ->where('original', $normalized) + ->first(); + + if ($existing) { + return $this->keywordCache[$cacheKey] = $existing->keyword; + } + + // πŸ‘‰ Local Extraction + $keyword = $this->extractKeyword($normalized); + + // πŸ‘‰ AI fallback (kontrolliert) + if ((!$keyword || strlen($keyword) < 4) && str_word_count($title) > 2) { + $keyword = $this->askAIForKeyword($title); + } + + // πŸ‘‰ sanitize + $keyword = $this->sanitizeKeyword($keyword); + + // πŸ‘‰ speichern + if ($keyword && strlen($keyword) >= 3) { + EventKeyword::updateOrCreate( + [ + 'user_id' => $userId, + 'original' => $normalized, + ], + [ + 'keyword' => $keyword, + ] + ); + } + + return $this->keywordCache[$cacheKey] = $keyword; + } + + /* + |-------------------------------------------------------------------------- + | NORMALIZE + |-------------------------------------------------------------------------- + */ + + protected function normalize(string $text): string + { + $text = strtolower($text); + + $text = preg_replace('/[^a-z0-9Àâüß\s]/u', '', $text); + $text = preg_replace('/\s+/', ' ', $text); + + return trim($text); + } + + /* + |-------------------------------------------------------------------------- + | EXTRACT KEYWORD (LOCAL) + |-------------------------------------------------------------------------- + */ + + protected function extractKeyword(string $text): ?string + { + $words = explode(' ', $text); + + $stopwords = [ + 'der','die','das','und','oder','von','im','am','um','zu', + 'ein','eine','einen','mit','fΓΌr','den','dem' + ]; + + $blacklist = ['gehen','machen','holen','fahren']; + + $filtered = array_filter($words, function ($word) use ($stopwords, $blacklist) { + return strlen($word) > 3 + && !in_array($word, $stopwords) + && !in_array($word, $blacklist); + }); + + if (empty($filtered)) { + return null; + } + + $filtered = array_values($filtered); + + // πŸ‘‰ priorisierte Begriffe + $priority = ['paket','arzt','friseur','meeting','lieferung','termin']; + + foreach ($priority as $p) { + if (in_array($p, $filtered)) { + return $p; + } + } + + return $filtered[0] ?? null; + } + + /* + |-------------------------------------------------------------------------- + | AI FALLBACK + |-------------------------------------------------------------------------- + */ + + protected function askAIForKeyword(string $title): string + { + try { + $response = app(OpenAIService::class)->chat([ + [ + 'role' => 'system', + 'content' => 'Extrahiere EIN generisches Keyword fΓΌr diesen Termin (z.B. "friseur", "paket", "arzt"). Nur ein Wort.' + ], + [ + 'role' => 'user', + 'content' => $title + ] + ]); + + return $this->sanitizeKeyword($response); + + } catch (\Throwable $e) { + return $this->extractKeyword($this->normalize($title)) ?? 'termin'; + } + } + + /* + |-------------------------------------------------------------------------- + | SANITIZE + |-------------------------------------------------------------------------- + */ + + protected function sanitizeKeyword(?string $text): string + { + if (!$text) return 'termin'; + + $text = strtolower(trim($text)); + + $text = explode(' ', $text)[0]; + $text = preg_replace('/[^a-z0-9Àâüß]/u', '', $text); + + return (strlen($text) >= 3) ? $text : 'termin'; + } +} diff --git a/src/app/Services/EventPlannerService.php b/src/app/Services/EventPlannerService.php new file mode 100644 index 0000000..efdf849 --- /dev/null +++ b/src/app/Services/EventPlannerService.php @@ -0,0 +1,863 @@ +timezone; + + /* + |-------------------------------------------------------------------------- + | 1. START + |-------------------------------------------------------------------------- + */ + + if (empty($data['start'])) { + return $this->fail('Kein Startdatum erkannt'); + } + + $start = Carbon::parse($data['start'], $tz); + + /* + |-------------------------------------------------------------------------- + | 2. DAUER + CONFIDENCE + |-------------------------------------------------------------------------- + */ + + $duration = max(5, $this->resolveDuration($user, $data)); + $confidence = $data['confidence'] ?? null; + + if ($confidence !== null && $confidence < 0.5) { + return [ + 'status' => 'needs_confirmation', + 'message' => 'Ich schΓ€tze ca. ' . $duration . ' Minuten. Passt das?', + 'meta' => [ + 'suggested_duration' => $duration, + 'start' => $start->format('Y-m-d H:i'), + ] + ]; + } + + /* + |-------------------------------------------------------------------------- + | 3. ALL DAY / NORMAL + |-------------------------------------------------------------------------- + */ + + $isAllDay = $data['is_all_day'] ?? false; + + if ($isAllDay) { + + $start = $start->copy()->startOfDay(); + + $end = !empty($data['end']) + ? Carbon::parse($data['end'], $tz)->endOfDay() + : $start->copy()->endOfDay(); + + $startUtc = $start->copy()->setTimezone('UTC'); + $endUtc = $end->copy()->setTimezone('UTC'); + + } else { + + // MehrtΓ€gig mit Zeiten: AI hat explizites End-Datum geliefert + if (!empty($data['end'])) { + $end = Carbon::parse($data['end'], $tz); + } else { + $end = $start->copy()->addMinutes($duration); + } + + $startUtc = $start->copy()->setTimezone('UTC'); + $endUtc = $end->copy()->setTimezone('UTC'); + } + + /* + |-------------------------------------------------------------------------- + | 4. KONFLIKT + FORCE LOGIK + |-------------------------------------------------------------------------- + */ + + $force = $data['force'] ?? false; + + $conflict = $this->hasConflict($user->id, $startUtc, $endUtc); + + if ($conflict && !$force) { + + $nextSlot = $this->findNextFreeSlot($user->id, $startUtc, $duration); + $prevSlot = $this->findPreviousFreeSlot($user->id, $startUtc, $duration); + + $next = $nextSlot?->copy()->setTimezone($tz); + $prev = $prevSlot?->copy()->setTimezone($tz); + + return [ + 'status' => 'conflict', + 'message' => 'Termin ΓΌberschneidet sich', + 'meta' => [ + 'start' => $start->format('Y-m-d H:i'), + 'end' => $end->format('d.m.Y H:i'), + 'suggestion_next' => $next?->format('Y-m-d H:i'), + 'suggestion_prev' => $prev?->format('Y-m-d H:i'), + 'duration' => $duration, + ] + ]; + } + + /* + |-------------------------------------------------------------------------- + | 5. SAVE + |-------------------------------------------------------------------------- + */ + + if ($this->isDuplicate($user->id, $startUtc, $endUtc)) { + return [ + 'status' => 'duplicate', + 'message' => 'Termin existiert bereits', + 'meta' => [] + ]; + } + + $eventData = [ + 'user_id' => $user->id, + 'title' => $data['title'] ?? 'Termin', + 'starts_at' => $startUtc, + 'ends_at' => $endUtc, + 'is_all_day' => $isAllDay, + 'exceptions' => $data['exceptions'] ?? [], + ]; + + if (!empty($data['notes'])) { + $eventData['notes'] = $data['notes']; + } + + $event = Event::create($eventData); + + #TODO Das mich spΓ€ter freischalten + +// if (!$isAllDay) { +// +// $durationToLearn = $data['duration_override'] ?? $duration; +// +// if ($durationToLearn >= 5 && $durationToLearn <= 180) { +// +// app(DurationLearningService::class) +// ->learn($user->id, $data['title'] ?? '', $durationToLearn); +// } +// } + + return [ + 'status' => 'success', + 'message' => $force + ? 'Termin trotz Konflikt gespeichert' + : 'Termin erstellt', + 'meta' => [ + 'title' => $event->title, + 'start' => $start->format('d.m.Y H:i'), + 'end' => $end->format('d.m.Y H:i'), + 'duration' => $duration, + ] + ]; + } + + /* + |-------------------------------------------------------------------------- + | DAUER + |-------------------------------------------------------------------------- + */ + + protected function resolveDuration($user, array $data): int + { + #TODO Das mich spΓ€ter freischalten + +// $learning = app(\App\Services\DurationLearningService::class); + + // πŸ‘‰ 1. USER override (z.B. aus Modal) + if (!empty($data['duration_override'])) { + return (int) $data['duration_override']; + } + + // πŸ‘‰ 2. LEARNING (immer davor!) +// if ($learned = $learning->get($user->id, $data['title'] ?? '')) { +// return $learned; +// } + + // πŸ‘‰ 3. AI / Parser duration + if (!empty($data['duration_minutes'])) { + + $ai = (int) $data['duration_minutes']; + + if ($ai >= 5 && $ai <= 180) { + return $ai; + } + } + + // πŸ‘‰ 4. fallback map + $map = [ + 'reifenwechsel' => 60, + 'friseur' => 45, + 'arzt' => 30, + 'zahnarzt' => 45, + 'meeting' => 60, + 'paket' => 10, + ]; + + $title = strtolower($data['title'] ?? ''); + + foreach ($map as $key => $minutes) { + if (str_contains($title, $key)) { + return $minutes; + } + } + + return 30; + } + + + /* + |-------------------------------------------------------------------------- + | KONFLIKT + |-------------------------------------------------------------------------- + */ + + protected function hasConflict(string $userId, Carbon $start, Carbon $end): bool + { + // 1. EintΓ€gige Termine: einfacher ZeitΓΌberlapp + $singleDayConflict = Event::where('user_id', $userId) + ->whereRaw('DATE(starts_at) = DATE(ends_at)') + ->where(function ($q) use ($start, $end) { + $q->where('starts_at', '<', $end) + ->where('ends_at', '>', $start); + }) + ->exists(); + + if ($singleDayConflict) { + return true; + } + + // 2. MehrtΓ€gige Termine (is_all_day oder regulΓ€r): Per-Day-Window-Projection + return $this->hasMultiDayConflict($userId, $start, $end); + } + + protected function hasMultiDayConflict(string $userId, Carbon $start, Carbon $end): bool + { + // Alle echten Mehrtagesevents deren Datumsbereich den neuen Termin ΓΌberschneidet + $candidates = Event::where('user_id', $userId) + ->whereRaw('DATE(starts_at) < DATE(ends_at)') + ->whereRaw('DATE(starts_at) <= ?', [$end->toDateString()]) + ->whereRaw('DATE(ends_at) >= ?', [$start->toDateString()]) + ->get(); + + foreach ($candidates as $event) { + // Reine ganztΓ€gige Termine ohne spezifische Zeiten ΓΌberspringen + if ($event->starts_at->format('H:i') === '00:00' + && in_array($event->ends_at->format('H:i'), ['23:59', '00:00'])) { + continue; + } + + // FΓΌr jeden Tag des neuen Termins prΓΌfen ob er ins Tagesfenster fΓ€llt + $cursor = $start->copy()->startOfDay(); + $lastDay = $end->copy()->startOfDay(); + + while ($cursor <= $lastDay) { + $eventStartDay = $event->starts_at->copy()->startOfDay(); + $eventEndDay = $event->ends_at->copy()->startOfDay(); + + if ($cursor >= $eventStartDay && $cursor <= $eventEndDay) { + // Tages-Fenster auf diesen konkreten Tag projizieren + $windowStart = $cursor->copy() + ->setHour($event->starts_at->hour) + ->setMinute($event->starts_at->minute) + ->setSecond(0); + + $windowEnd = $cursor->copy() + ->setHour($event->ends_at->hour) + ->setMinute($event->ends_at->minute) + ->setSecond(0); + + // Anteil des neuen Termins an diesem Tag + $segStart = $start->copy()->max($cursor->copy()->startOfDay()); + $segEnd = $end->copy()->min($cursor->copy()->endOfDay()); + + if ($segStart < $windowEnd && $segEnd > $windowStart) { + return true; + } + } + + $cursor->addDay(); + } + } + + return false; + } + + /* + |-------------------------------------------------------------------------- + | SLOT FINDEN + |-------------------------------------------------------------------------- + */ + + protected function getBlockingEvent(string $userId, Carbon $start, Carbon $end): ?Event + { + // 1. EintΓ€gige Termine: einfacher ZeitΓΌberlapp + $single = Event::where('user_id', $userId) + ->whereRaw('DATE(starts_at) = DATE(ends_at)') + ->where(function ($q) use ($start, $end) { + $q->where('starts_at', '<', $end) + ->where('ends_at', '>', $start); + }) + ->orderBy('ends_at') + ->first(); + + if ($single) { + return $single; + } + + // 2. MehrtΓ€gige Termine: Per-Day-Window β€” blockierendes Tagesfenster zurΓΌckgeben + $candidates = Event::where('user_id', $userId) + ->whereRaw('DATE(starts_at) < DATE(ends_at)') + ->whereRaw('DATE(starts_at) <= ?', [$end->toDateString()]) + ->whereRaw('DATE(ends_at) >= ?', [$start->toDateString()]) + ->get(); + + foreach ($candidates as $event) { + if ($event->starts_at->format('H:i') === '00:00' + && in_array($event->ends_at->format('H:i'), ['23:59', '00:00'])) { + continue; + } + + $cursor = $start->copy()->startOfDay(); + $lastDay = $end->copy()->startOfDay(); + + while ($cursor <= $lastDay) { + $eventStartDay = $event->starts_at->copy()->startOfDay(); + $eventEndDay = $event->ends_at->copy()->startOfDay(); + + if ($cursor >= $eventStartDay && $cursor <= $eventEndDay) { + $windowStart = $cursor->copy() + ->setHour($event->starts_at->hour) + ->setMinute($event->starts_at->minute) + ->setSecond(0); + + $windowEnd = $cursor->copy() + ->setHour($event->ends_at->hour) + ->setMinute($event->ends_at->minute) + ->setSecond(0); + + $segStart = $start->copy()->max($cursor->copy()->startOfDay()); + $segEnd = $end->copy()->min($cursor->copy()->endOfDay()); + + if ($segStart < $windowEnd && $segEnd > $windowStart) { + // starts_at = Fenster-Start, ends_at = Fenster-Ende fΓΌr diesen Tag + $event->starts_at = $windowStart; + $event->ends_at = $windowEnd; + return $event; + } + } + + $cursor->addDay(); + } + } + + return null; + } + + protected function findPreviousFreeSlot(string $userId, Carbon $start, int $duration): ?Carbon + { + $cursor = $start->copy(); + + $limit = 0; + + while ($limit < 50) { + + $candidateStart = $cursor->copy()->subMinutes($duration); + $candidateEnd = $cursor; + + $blocking = $this->getBlockingEvent($userId, $candidateStart, $candidateEnd); + + if (!$blocking) { + return $candidateStart; + } + + if (!$blocking->starts_at) { + return $candidateStart; + } + + // πŸ‘‰ nach hinten springen + $cursor = $blocking->starts_at->copy(); + + $limit++; + } + + return null; + } + + protected function findNextFreeSlot(string $userId, Carbon $start, int $duration): ?Carbon + { + $cursor = $start->copy(); + + $limit = 0; + + while ($limit < 50) { + + $end = $cursor->copy()->addMinutes($duration); + + $blocking = $this->getBlockingEvent($userId, $cursor, $end); + + if (!$blocking) { + return $cursor; + } + + if (!$blocking->ends_at) { + return $cursor; + } + + $cursor = $blocking->ends_at->copy(); + + $limit++; + } + + return null; + } + + protected function fail(string $message): array + { + return [ + 'status' => 'failed', + 'message' => $message, + 'meta' => [] + ]; + } + + protected function isDuplicate(string $userId, Carbon $start, Carbon $end): bool + { + return Event::where('user_id', $userId) + ->where('is_all_day', false) + ->where('starts_at', $start) + ->where('ends_at', $end) + ->exists(); + } +} + + + +// protected function findNextFreeSlot(string $userId, Carbon $start, int $duration): ?Carbon +// { +// $cursor = $start->copy(); +// +// for ($i = 0; $i < 20; $i++) { +// +// $end = $cursor->copy()->addMinutes($duration); +// +// if (!$this->hasConflict($userId, $cursor, $end)) { +// return $cursor; +// } +// +// $cursor->addMinutes(15); +// } +// +// return null; +// } + +// +// +//namespace App\Services; +// +//use App\Models\Event; +//use Carbon\Carbon; +// +//class EventPlannerService +//{ +// public function plan($user, array $data): array +// { +// $tz = $user->timezone; +// +// /* +// |-------------------------------------------------------------------------- +// | 1. START +// |-------------------------------------------------------------------------- +// */ +// +// if (empty($data['start'])) { +// return $this->fail('Kein Startdatum erkannt'); +// } +// +// $start = Carbon::parse($data['start'], $tz); +// +// /* +// |-------------------------------------------------------------------------- +// | 2. DAUER + CONFIDENCE +// |-------------------------------------------------------------------------- +// */ +// +// $duration = max(5, $this->resolveDuration($data)); +// $confidence = $data['confidence'] ?? null; +// +// if ($confidence !== null && $confidence < 0.5) { +// return [ +// 'status' => 'needs_confirmation', +// 'message' => 'Ich schΓ€tze ca. ' . $duration . ' Minuten. Passt das?', +// 'meta' => [ +// 'suggested_duration' => $duration, +// 'start' => $start->format('Y-m-d H:i'), +// ] +// ]; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | 3. ALL DAY / NORMAL +// |-------------------------------------------------------------------------- +// */ +// +// $isAllDay = $data['is_all_day'] ?? false; +// +// if ($isAllDay) { +// $start->setTime(0, 0); +// $end = $start->copy()->endOfDay(); +// } else { +// $end = $start->copy()->addMinutes($duration); +// } +// +// /* +// |-------------------------------------------------------------------------- +// | 4. KONFLIKT +// |-------------------------------------------------------------------------- +// */ +// +// $startUtc = $start->copy()->setTimezone('UTC'); +// $endUtc = $end->copy()->setTimezone('UTC'); +// +// $conflict = $this->hasConflict($user->id, $startUtc, $endUtc); +// +// if ($conflict) { +// +// $nextSlot = $this->findNextFreeSlot($user->id, $startUtc, $duration); +// +// return [ +// 'status' => 'conflict', +// 'message' => 'Termin ΓΌberschneidet sich', +// 'suggestion' => $nextSlot +// ? $nextSlot->format('d.m.Y H:i') +// : null, +// 'meta' => [ +// 'start' => $start->toDateTimeString(), +// 'end' => $end->toDateTimeString(), +// ] +// ]; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | 5. SAVE +// |-------------------------------------------------------------------------- +// */ +// +// $event = Event::create([ +// 'user_id' => $user->id, +// 'title' => $data['title'] ?? 'Termin', +// 'starts_at' => $start->copy()->setTimezone('UTC'), +// 'ends_at' => $end->copy()->setTimezone('UTC'), +// 'is_all_day' => $isAllDay, +// ]); +// +// return [ +// 'status' => 'success', +// 'message' => 'Termin erstellt', +// 'meta' => [ +// 'title' => $event->title, +// 'start' => $start->format('d.m.Y H:i'), +// 'end' => $end->format('d.m.Y H:i'), +// 'duration' => $duration, +// ] +// ]; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | DAUER +// |-------------------------------------------------------------------------- +// */ +// +// protected function resolveDuration(array $data): int +// { +// if (!empty($data['duration_minutes'])) { +// return (int)$data['duration_minutes']; +// } +// +// $map = [ +// 'reifenwechsel' => 60, +// 'friseur' => 45, +// 'arzt' => 30, +// 'zahnarzt' => 45, +// 'meeting' => 60, +// ]; +// +// $title = strtolower($data['title'] ?? ''); +// +// foreach ($map as $key => $minutes) { +// if (str_contains($title, $key)) { +// return $minutes; +// } +// } +// +// if (!empty($data['ai_duration'])) { +// return (int)$data['ai_duration']; +// } +// +// return 60; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | KONFLIKT +// |-------------------------------------------------------------------------- +// */ +// +// protected function hasConflict(string $userId, Carbon $start, Carbon $end): bool +// { +// return Event::where('user_id', $userId) +// ->where(function ($q) use ($start, $end) { +// $q->whereBetween('starts_at', [$start, $end]) +// ->orWhereBetween('ends_at', [$start, $end]) +// ->orWhere(function ($q) use ($start, $end) { +// $q->where('starts_at', '<=', $start) +// ->where('ends_at', '>=', $end); +// }); +// }) +// ->exists(); +// } +// +// /* +// |-------------------------------------------------------------------------- +// | SLOT FINDEN +// |-------------------------------------------------------------------------- +// */ +// +// protected function findNextFreeSlot(string $userId, Carbon $start, int $duration): ?Carbon +// { +// $cursor = $start->copy(); +// +// for ($i = 0; $i < 20; $i++) { +// +// $end = $cursor->copy()->addMinutes($duration); +// +// if (!$this->hasConflict($userId, $cursor, $end)) { +// return $cursor; +// } +// +// $cursor->addMinutes(15); +// } +// +// return null; +// } +// +// protected function fail(string $message): array +// { +// return [ +// 'status' => 'failed', +// 'message' => $message, +// 'meta' => [] +// ]; +// } +//} + +// +//namespace App\Services; +// +//use App\Models\Event; +//use Carbon\Carbon; +// +//class EventPlannerService +//{ +// public function plan($user, array $data): array +// { +// $tz = $user->timezone; +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ 1. START ZEIT ERMITTELN +// |-------------------------------------------------------------------------- +// */ +// +// if (empty($data['start'])) { +// return $this->fail('Kein Startdatum erkannt'); +// } +// +// $start = Carbon::parse($data['start'], $tz); +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ 2. DAUER BESTIMMEN +// |-------------------------------------------------------------------------- +// */ +// +// $duration = max(5, $this->resolveDuration($data)); +// $confidence = $data['confidence'] ?? null; +// +// if ($confidence !== null && $confidence < 0.5) { +// return [ +// 'status' => 'needs_confirmation', +// 'message' => 'Ich schΓ€tze ca. ' . $duration . ' Minuten. Passt das?', +// 'meta' => [ +// 'suggested_duration' => $duration, +// 'start' => $start->format('Y-m-d H:i'), +// ] +// ]; +// } +// +// $end = $start->copy()->addMinutes($duration); +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ 3. KONFLIKTE PRÜFEN +// |-------------------------------------------------------------------------- +// */ +// +// $conflict = $this->hasConflict($user->id, $start, $end); +// +// if ($conflict) { +// +// $nextSlot = $this->findNextFreeSlot($user->id, $start, $duration); +// +// return [ +// 'status' => 'conflict', +// 'message' => 'Termin ΓΌberschneidet sich', +// 'suggestion' => $nextSlot +// ? $nextSlot->format('d.m.Y H:i') +// : null, +// 'meta' => [ +// 'start' => $start, +// 'end' => $end, +// ] +// ]; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ 4. EVENT ERSTELLEN +// |-------------------------------------------------------------------------- +// */ +// +// $isAllDay = $data['is_all_day'] ?? false; +// +// if ($isAllDay) { +// $start->setTime(0,0); +// $end = $start->copy()->endOfDay(); +// } +// +// $event = Event::create([ +// 'user_id' => $user->id, +// 'title' => $data['title'] ?? 'Termin', +// 'starts_at' => $start->copy()->setTimezone('UTC'), +// 'ends_at' => $end->copy()->setTimezone('UTC'), +// 'is_all_day'=> false, +// ]); +// +// return [ +// 'status' => 'success', +// 'message' => 'Termin erstellt', +// 'meta' => [ +// 'title' => $event->title, +// 'start' => $start->format('d.m.Y H:i'), +// 'end' => $end->format('d.m.Y H:i'), +// 'duration' => $duration, +// ] +// ]; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ DAUER LOGIK +// |-------------------------------------------------------------------------- +// */ +// +// protected function resolveDuration(array $data): int +// { +// // πŸ”₯ 1. Explizit ΓΌbergeben +// if (!empty($data['duration_minutes'])) { +// return (int) $data['duration_minutes']; +// } +// +// // πŸ”₯ 2. Mapping (WICHTIG!) +// $map = [ +// 'reifenwechsel' => 60, +// 'friseur' => 45, +// 'arzt' => 30, +// 'zahnarzt' => 45, +// 'meeting' => 60, +// ]; +// +// $title = strtolower($data['title'] ?? ''); +// +// foreach ($map as $key => $minutes) { +// if (str_contains($title, $key)) { +// return $minutes; +// } +// } +// +// // πŸ”₯ 3. AI fallback +// if (!empty($data['ai_duration'])) { +// return (int) $data['ai_duration']; +// } +// +// // πŸ”₯ DEFAULT +// return 60; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ KONFLIKT CHECK +// |-------------------------------------------------------------------------- +// */ +// +// protected function hasConflict(string $userId, Carbon $start, Carbon $end): bool +// { +// return Event::where('user_id', $userId) +// ->where(function ($q) use ($start, $end) { +// $q->whereBetween('starts_at', [$start, $end]) +// ->orWhereBetween('ends_at', [$start, $end]) +// ->orWhere(function ($q) use ($start, $end) { +// $q->where('starts_at', '<=', $start) +// ->where('ends_at', '>=', $end); +// }); +// }) +// ->exists(); +// } +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ NΓ„CHSTER FREIER SLOT +// |-------------------------------------------------------------------------- +// */ +// +// protected function findNextFreeSlot(string $userId, Carbon $start, int $duration): ?Carbon +// { +// $cursor = $start->copy(); +// +// for ($i = 0; $i < 20; $i++) { +// +// $end = $cursor->copy()->addMinutes($duration); +// +// if (!$this->hasConflict($userId, $cursor, $end)) { +// return $cursor; +// } +// +// $cursor->addMinutes(15); +// } +// +// return null; +// } +// +// protected function fail(string $message): array +// { +// return [ +// 'status' => 'failed', +// 'message' => $message, +// 'meta' => [] +// ]; +// } +//} diff --git a/src/app/Services/GoogleCalendarService.php b/src/app/Services/GoogleCalendarService.php new file mode 100644 index 0000000..bf76e3f --- /dev/null +++ b/src/app/Services/GoogleCalendarService.php @@ -0,0 +1,459 @@ +isExpired() && $integration->refresh_token) { + $this->refreshToken($integration); + $integration->refresh(); + } + + return $integration->rawAccessToken(); + } + + private function refreshToken(CalendarIntegration $integration): void + { + $response = Http::asForm()->post(self::TOKEN_URL, [ + 'client_id' => config('services.google_calendar.client_id'), + 'client_secret' => config('services.google_calendar.client_secret'), + 'refresh_token' => $integration->refresh_token, + 'grant_type' => 'refresh_token', + ]); + + if ($response->failed()) { + Log::error('Google Calendar: Token-Refresh fehlgeschlagen', [ + 'user_id' => $integration->user_id, + 'status' => $response->status(), + ]); + return; + } + + $data = $response->json(); + + $integration->update([ + 'access_token' => $data['access_token'], + 'token_expires_at' => now()->addSeconds($data['expires_in'] ?? 3600), + ]); + } + + // ── Push: Lokal β†’ Google ────────────────────────────────────────────── + + /** + * Erstellt oder aktualisiert ein Event in Google Calendar. + * Setzt google_event_id auf dem lokalen Event. + */ + public function pushEvent(Event $event, CalendarIntegration $integration): void + { + $token = $this->getValidToken($integration); + if (!$token) return; + + $userTz = User::find($integration->user_id)?->timezone ?? config('app.timezone', 'UTC'); + $body = $this->toGoogleEvent($event, $userTz); + $calendarId = urlencode($integration->calendar_id ?? 'primary'); + + Log::info('Google Calendar: Push-Debug', [ + 'event_id' => $event->id, + 'db_start' => $event->starts_at?->toIso8601String(), + 'user_tz' => $userTz, + 'google_start' => $body['start']['dateTime'] ?? $body['start']['date'] ?? null, + 'google_tz' => $body['start']['timeZone'] ?? null, + ]); + + if ($event->google_event_id) { + // Update + $response = Http::withToken($token) + ->put(self::CALENDAR_API."/{$calendarId}/events/{$event->google_event_id}", $body); + } else { + // Create + $response = Http::withToken($token) + ->post(self::CALENDAR_API."/{$calendarId}/events", $body); + } + + if ($response->failed()) { + Log::error('Google Calendar: Push fehlgeschlagen', [ + 'user_id' => $integration->user_id, + 'event_id' => $event->id, + 'status' => $response->status(), + 'body' => $response->body(), + ]); + return; + } + + $googleId = $response->json('id'); + + if ($googleId && $googleId !== $event->google_event_id) { + // Ohne Observer-Trigger direkt in DB schreiben (forceFill umgeht Mass-Assignment) + Event::withoutEvents(function () use ($event, $googleId) { + $event->forceFill(['google_event_id' => $googleId])->save(); + }); + } + } + + /** + * LΓΆscht ein Event in Google Calendar. + */ + public function deleteEvent(string $googleEventId, CalendarIntegration $integration): void + { + $token = $this->getValidToken($integration); + if (!$token) return; + + $calendarId = urlencode($integration->calendar_id ?? 'primary'); + + $response = Http::withToken($token) + ->delete(self::CALENDAR_API."/{$calendarId}/events/{$googleEventId}"); + + if ($response->failed() && $response->status() !== 410) { + Log::error('Google Calendar: Delete fehlgeschlagen', [ + 'user_id' => $integration->user_id, + 'google_event_id' => $googleEventId, + 'status' => $response->status(), + ]); + } + } + + // ── Pull: Google β†’ Lokal ────────────────────────────────────────────── + + /** + * Importiert alle neuen/geΓ€nderten Events von Google Calendar. + * Nutzt nextSyncToken fΓΌr inkrementellen Sync; fΓ€llt auf Vollsync zurΓΌck bei 410. + */ + public function pullEvents(CalendarIntegration $integration): void + { + $token = $this->getValidToken($integration); + if (!$token) { + Log::warning('Google Calendar: Kein gΓΌltiger Token fΓΌr Pull-Sync', [ + 'user_id' => $integration->user_id, + 'is_expired' => $integration->isExpired(), + ]); + return; + } + + $user = User::find($integration->user_id); + if (!$user) return; + + $calendarId = urlencode($integration->calendar_id ?? 'primary'); + $url = self::CALENDAR_API."/{$calendarId}/events"; + + // WICHTIG: orderBy/singleEvents dΓΌrfen nicht zusammen mit syncToken verwendet werden + // und orderBy verhindert die RΓΌckgabe von nextSyncToken auch beim Vollsync. + if ($integration->rawSyncToken()) { + // Inkrementeller Sync: NUR syncToken, keine weiteren Filter (Google-Vorschrift) + $params = [ + 'maxResults' => 250, + 'syncToken' => $integration->rawSyncToken(), + 'showDeleted' => 'true', + ]; + } else { + // Erster Vollsync: singleEvents ohne orderBy β†’ Google liefert nextSyncToken + $params = [ + 'maxResults' => 250, + 'singleEvents' => 'true', + 'timeMin' => now()->subDays(30)->toRfc3339String(), + ]; + } + + self::$syncing = true; + + try { + $imported = $this->fetchAndImportPages($url, $params, $token, $integration, $user); + + if ($imported > 0) { + broadcast(new CalendarUpdated($user->id)); + } + } catch (\Exception $e) { + Log::error('Google Calendar: Pull fehlgeschlagen', [ + 'user_id' => $integration->user_id, + 'error' => $e->getMessage(), + ]); + } finally { + self::$syncing = false; + } + } + + private function fetchAndImportPages( + string $url, + array $params, + string $token, + CalendarIntegration $integration, + User $user + ): int { + $pageToken = null; + $importCount = 0; + + do { + $query = $params; + if ($pageToken) { + $query['pageToken'] = $pageToken; + } + + $response = Http::withToken($token)->get($url, $query); + + // 410 Gone = syncToken ungΓΌltig β†’ Vollsync + if ($response->status() === 410) { + Log::info('Google Calendar: syncToken abgelaufen, starte Vollsync', [ + 'user_id' => $integration->user_id, + ]); + $integration->forceFill(['sync_token' => null])->save(); + unset($params['syncToken']); + $params['timeMin'] = now()->subDays(30)->toRfc3339String(); + $response = Http::withToken($token)->get($url, $params); + } + + if ($response->failed()) { + Log::error('Google Calendar: API-Fehler beim Pull', [ + 'user_id' => $integration->user_id, + 'status' => $response->status(), + ]); + break; + } + + $data = $response->json(); + $items = $data['items'] ?? []; + + Log::info('Google Calendar: Pull-Antwort', [ + 'user_id' => $integration->user_id, + 'status' => $response->status(), + 'items' => count($items), + 'has_sync' => isset($params['syncToken']), + 'has_next' => !empty($data['nextSyncToken']), + ]); + + foreach ($items as $item) { + try { + $this->importGoogleEvent($item, $user); + $importCount++; + Log::info('Google Calendar: Event importiert', [ + 'user_id' => $user->id, + 'google_event_id' => $item['id'] ?? null, + 'summary' => $item['summary'] ?? null, + 'status' => $item['status'] ?? null, + ]); + } catch (\Throwable $e) { + Log::error('Google Calendar: Einzelnes Event konnte nicht importiert werden', [ + 'user_id' => $user->id, + 'event_id' => $item['id'] ?? null, + 'summary' => $item['summary'] ?? null, + 'error' => $e->getMessage(), + ]); + } + } + + $pageToken = $data['nextPageToken'] ?? null; + + // nextSyncToken nur auf der letzten Seite gesetzt + if (!empty($data['nextSyncToken'])) { + $integration->update([ + 'sync_token' => $data['nextSyncToken'], + 'last_synced_at' => now(), + ]); + } + + } while ($pageToken); + + return $importCount; + } + + private function importGoogleEvent(array $item, User $user): void + { + $googleId = $item['id'] ?? null; + if (!$googleId) return; + + // Abgesagte Events lokal lΓΆschen + if (($item['status'] ?? '') === 'cancelled') { + Event::withoutEvents( + fn () => Event::where('user_id', $user->id) + ->where('google_event_id', $googleId) + ->delete() + ); + return; + } + + $mapped = $this->fromGoogleEvent($item); + if (!$mapped) return; // Event ohne Zeitangabe ΓΌberspringen + + $existing = Event::where('user_id', $user->id) + ->where('google_event_id', $googleId) + ->first(); + + Event::withoutEvents(function () use ($existing, $mapped, $user, $googleId) { + if ($existing) { + $existing->forceFill($mapped)->save(); + } else { + (new Event)->forceFill(array_merge($mapped, [ + 'id' => (string) \Illuminate\Support\Str::uuid(), + 'user_id' => $user->id, + 'google_event_id' => $googleId, + ]))->save(); + } + }); + } + + // ── Watch: Google Push-Benachrichtigungen ───────────────────────────── + + /** + * Registriert einen Google Watch-Kanal. + * Google sendet dann einen POST-Webhook bei jeder KalenderΓ€nderung. + * Watch lΓ€uft max. 7 Tage β€” muss vor Ablauf erneuert werden. + */ + public function createWatch(CalendarIntegration $integration): void + { + $token = $this->getValidToken($integration); + if (!$token) return; + + // Alten Watch stoppen falls vorhanden + $this->stopWatch($integration); + + $channelId = (string) \Illuminate\Support\Str::uuid(); + $calendarId = urlencode($integration->calendar_id ?? 'primary'); + $webhookUrl = config('services.google_calendar.webhook_url') ?? route('webhooks.google-calendar'); + + $response = Http::withToken($token) + ->post(self::CALENDAR_API."/{$calendarId}/events/watch", [ + 'id' => $channelId, + 'type' => 'web_hook', + 'address' => $webhookUrl, + ]); + + if ($response->failed()) { + Log::error('Google Calendar: Watch-Registrierung fehlgeschlagen', [ + 'user_id' => $integration->user_id, + 'status' => $response->status(), + 'body' => $response->body(), + ]); + return; + } + + $data = $response->json(); + + // Ablaufzeit kommt in Millisekunden + $expiresAt = isset($data['expiration']) + ? \Carbon\Carbon::createFromTimestampMs($data['expiration']) + : now()->addDays(7); + + $integration->update([ + 'watch_channel_id' => $data['id'] ?? $channelId, + 'watch_resource_id' => $data['resourceId'] ?? null, + 'watch_expires_at' => $expiresAt, + ]); + + Log::info('Google Calendar: Watch registriert', [ + 'user_id' => $integration->user_id, + 'channel_id' => $data['id'] ?? $channelId, + 'expires_at' => $expiresAt->toDateTimeString(), + ]); + } + + /** + * Stoppt einen aktiven Watch-Kanal bei Google. + */ + public function stopWatch(CalendarIntegration $integration): void + { + if (!$integration->watch_channel_id || !$integration->watch_resource_id) { + return; + } + + $token = $this->getValidToken($integration); + if (!$token) return; + + Http::withToken($token)->post(self::CHANNELS_STOP, [ + 'id' => $integration->watch_channel_id, + 'resourceId' => $integration->watch_resource_id, + ]); + + $integration->update([ + 'watch_channel_id' => null, + 'watch_resource_id' => null, + 'watch_expires_at' => null, + ]); + } + + // ── Feld-Mapping ────────────────────────────────────────────────────── + + /** + * Konvertiert ein lokales Event in das Google Calendar API-Format. + */ + private function toGoogleEvent(Event $event, string $timezone): array + { + if ($event->is_all_day) { + return [ + 'summary' => $event->title, + 'description' => $event->notes, + 'start' => ['date' => $event->starts_at->toDateString()], + 'end' => ['date' => ($event->ends_at ?? $event->starts_at)->toDateString()], + ]; + } + + $start = $event->starts_at->copy()->setTimezone($timezone); + $end = ($event->ends_at ?? $event->starts_at->copy()->addHour())->copy()->setTimezone($timezone); + + return [ + 'summary' => $event->title, + 'description' => $event->notes, + 'start' => [ + 'dateTime' => $start->toRfc3339String(), + 'timeZone' => $timezone, + ], + 'end' => [ + 'dateTime' => $end->toRfc3339String(), + 'timeZone' => $timezone, + ], + ]; + } + + /** + * Konvertiert ein Google Calendar Event in lokale Felder. + * Gibt null zurΓΌck wenn keine gΓΌltige Zeitangabe vorhanden. + */ + private function fromGoogleEvent(array $item): ?array + { + $isAllDay = isset($item['start']['date']); + $startsRaw = $item['start']['dateTime'] ?? $item['start']['date'] ?? null; + $endsRaw = $item['end']['dateTime'] ?? $item['end']['date'] ?? null; + + if (!$startsRaw) return null; + + $startTz = $item['start']['timeZone'] ?? 'UTC'; + $endTz = $item['end']['timeZone'] ?? $startTz; + $startsAt = Carbon::parse($startsRaw, $startTz)->utc(); + $endsAt = $endsRaw ? Carbon::parse($endsRaw, $endTz)->utc() : null; + + Log::info('Google Calendar: Pull-Debug', [ + 'google_event_id' => $item['id'] ?? null, + 'raw_start' => $startsRaw, + 'raw_tz' => $startTz, + 'parsed_utc' => $startsAt->toIso8601String(), + ]); + + return [ + 'title' => $item['summary'] ?? '(Kein Titel)', + 'notes' => $item['description'] ?? null, + 'is_all_day' => $isAllDay, + 'starts_at' => $startsAt, + 'ends_at' => $endsAt, + 'start_date' => $isAllDay ? $startsAt->toDateString() : null, + 'end_date' => ($isAllDay && $endsAt) ? $endsAt->toDateString() : null, + ]; + } +} diff --git a/src/app/Services/MailService.php b/src/app/Services/MailService.php new file mode 100644 index 0000000..dfe2c8c --- /dev/null +++ b/src/app/Services/MailService.php @@ -0,0 +1,42 @@ +where('template', 'auth.verify') + ->whereIn('status', ['pending', 'processing']) + ->update([ + 'status' => 'canceled' + ]); + } + + MailQueue::create([ + 'user_id' => $userId, + 'to' => $to, + 'subject' => $subject ?? self::resolveSubject($template, $locale), + 'template' => $template, + 'meta' => $meta, + 'locale' => $locale, + 'available_at' => now(), + 'status' => 'pending', + ]); + + } + + protected static function resolveSubject($template, $locale) + { + return match ($template) { + 'auth.verify' => t('mail.auth.verify.subject', [], $locale), + default => 'Notification', + }; + } + +} diff --git a/src/app/Services/MailerService.php b/src/app/Services/MailerService.php new file mode 100644 index 0000000..c0e00e6 --- /dev/null +++ b/src/app/Services/MailerService.php @@ -0,0 +1,131 @@ + 'reminder', + + 'plan_activated', + 'credits_low', + 'credits_added' => 'system', + + 'aria_composed' => 'aria', + + 'welcome', + 'onboarding' => 'hello', + + default => 'smtp', + }; + } + + public static function channelsForAutomation(string $type): array + { + return match ($type) { + 'event_reminder' => ['push'], + 'daily_agenda' => ['push', 'email'], + 'weekly_overview' => ['email'], + 'event_followup' => ['push'], + 'birthday_reminder' => ['push'], + 'no_activity_reminder' => ['push'], + 'daily_summary' => ['email'], + 'contact_followup' => ['push'], + 'free_slots_report' => ['email'], + default => ['email'], + }; + } + + public static function pushTextForAutomation(string $type, array $data = []): array + { + return match ($type) { + 'event_reminder' => [ + 'title' => 'Erinnerung: ' . ($data['title'] ?? 'Termin'), + 'body' => match (true) { + ($data['minutes'] ?? 0) >= 1440 => 'Morgen um ' . ($data['time'] ?? '') . ' Uhr', + ($data['minutes'] ?? 0) >= 60 => 'In ' . (($data['minutes'] ?? 60) / 60) . ' Stunde(n)', + default => 'In ' . ($data['minutes'] ?? 30) . ' Minuten', + }, + ], + 'daily_agenda' => [ + 'title' => 'Deine Agenda β€” ' . ($data['date'] ?? 'Heute'), + 'body' => ($data['count'] ?? 0) . ' Termine heute Β· erster um ' . ($data['first_time'] ?? ''), + ], + 'birthday_reminder' => [ + 'title' => 'Geburtstag: ' . ($data['name'] ?? ''), + 'body' => ($data['days'] ?? 0) === 0 + ? 'Heute ist sein/ihr Geburtstag!' + : 'In ' . ($data['days'] ?? 1) . ' Tag(en)', + ], + 'event_followup' => [ + 'title' => 'Nachbereitung', + 'body' => ($data['title'] ?? 'Termin') . ' ist beendet', + ], + 'no_activity_reminder' => [ + 'title' => 'Keine Termine', + 'body' => 'Du hattest ' . ($data['days'] ?? 3) . ' Tage keine Termine', + ], + 'contact_followup' => [ + 'title' => 'Kontakt-Erinnerung', + 'body' => ($data['name'] ?? 'Kontakt') . ' seit ' . ($data['days'] ?? 14) . ' Tagen nicht kontaktiert', + ], + default => [ + 'title' => 'Aziros', + 'body' => 'Neue Benachrichtigung', + ], + }; + } + + public static function sendViaUserSmtp(User $user, string $to, Mailable $mail): bool + { + if (!RateLimiter::attempt('user-smtp:' . $user->id, 10, function () {})) { + \Log::warning('SMTP Rate limit hit', ['user_id' => $user->id]); + return false; + } + + $config = $user->smtp_settings ?? []; + if (empty($config['host'])) { + return false; + } + + config(['mail.mailers.user_smtp_' . $user->id => [ + 'transport' => 'smtp', + 'host' => $config['host'], + 'port' => $config['port'] ?? 587, + 'encryption' => $config['encryption'] ?? 'tls', + 'username' => $config['username'], + 'password' => decrypt($config['password']), + 'from' => [ + 'address' => $config['from_address'] ?? $user->email, + 'name' => $config['from_name'] ?? $user->name, + ], + ]]); + + Mail::mailer('user_smtp_' . $user->id) + ->to($to) + ->send($mail); + + $user->emailLogs()->create([ + 'to' => $to, + 'subject' => method_exists($mail, 'envelope') ? $mail->envelope()->subject : 'E-Mail', + 'type' => 'user_smtp', + ]); + + return true; + } +} diff --git a/src/app/Services/PushService.php b/src/app/Services/PushService.php new file mode 100644 index 0000000..5e064c7 --- /dev/null +++ b/src/app/Services/PushService.php @@ -0,0 +1,87 @@ +id) + ->where('active', true) + ->whereNotNull('push_token') + ->pluck('push_token'); + + Log::info('PushService: tokens', [ + 'user_id' => $user->id, + 'count' => $tokens->count(), + 'tokens' => $tokens->toArray(), + ]); + + if ($tokens->isEmpty()) { + return; + } + + $messages = $tokens->map(fn ($token) => [ + 'to' => $token, + 'title' => $title, + 'body' => $body, + 'data' => $data, + 'sound' => 'default', + 'badge' => 1, + ])->values()->toArray(); + + try { + $response = Http::withHeaders([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ])->post(self::EXPO_PUSH_URL, $messages); + + $receipts = $response->json(); + + Log::info('PushService: response', [ + 'status' => $response->status(), + 'body' => $receipts, + ]); + + Log::info('Push sent', [ + 'user_id' => $user->id, + 'tokens' => $tokens->count(), + 'response' => $receipts, + ]); + + // UngΓΌltige Tokens sofort deaktivieren + if (is_array($receipts)) { + self::handleReceipts($receipts, $tokens->toArray()); + } + } catch (\Exception $e) { + Log::error('Push failed', [ + 'user_id' => $user->id, + 'error' => $e->getMessage(), + ]); + } + } + + public static function handleReceipts(array $receipts, array $tokens): void + { + foreach ($receipts as $i => $receipt) { + if (($receipt['status'] ?? '') === 'error') { + $details = $receipt['details'] ?? []; + if (($details['error'] ?? '') === 'DeviceNotRegistered') { + Device::where('push_token', $tokens[$i] ?? '') + ->update(['active' => false]); + } + } + } + } +} diff --git a/src/app/Services/StripeService.php b/src/app/Services/StripeService.php new file mode 100644 index 0000000..0f60910 --- /dev/null +++ b/src/app/Services/StripeService.php @@ -0,0 +1,924 @@ +stripe = new StripeClient(config('services.stripe.secret')); + } + + /* + |-------------------------------------------------------------------------- + | CHECKOUT SESSION + |-------------------------------------------------------------------------- + */ + + public function createCheckoutSession(User $user, Plan $plan, string $billing): Session + { + $currency = config('services.stripe.currency', 'eur'); + $price = $billing === 'yearly' ? $plan->getYearlyPrice() : $plan->getMonthlyPrice(); + + $lineItem = [ + 'price_data' => [ + 'currency' => $currency, + 'product_data' => [ + 'name' => $plan->name, + 'description' => $billing === 'yearly' ? 'JΓ€hrliches Abonnement' : 'Monatliches Abonnement', + ], + 'unit_amount' => $price, + 'recurring' => [ + 'interval' => $billing === 'yearly' ? 'year' : 'month', + ], + ], + 'quantity' => 1, + ]; + + // Stripe-Kunden-ID aus der User-Subscription holen (bleibt auch auf Free-Plan erhalten), + // damit das bestehende Stripe-Kundenkonto (inkl. Guthaben) immer wiederverwendet wird. + $existingCustomerId = Subscription::where('user_id', $user->id) + ->whereNotNull('provider_customer_id') + ->latest('created_at') + ->value('provider_customer_id'); + + $sharedMeta = [ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing' => $billing, + ]; + + $params = [ + 'mode' => 'subscription', + 'payment_method_types' => ['card'], + 'line_items' => [$lineItem], + 'success_url' => route('checkout.success') . '?session_id={CHECKOUT_SESSION_ID}', + 'cancel_url' => route('subscription.index'), + 'client_reference_id' => $user->id, + 'metadata' => $sharedMeta, + 'subscription_data' => ['metadata' => $sharedMeta], + 'locale' => 'de', + 'allow_promotion_codes'=> true, + ]; + + if ($existingCustomerId) { + // Bestehenden Kunden ΓΌbergeben β†’ Guthaben wird automatisch verrechnet + $params['customer'] = $existingCustomerId; + } else { + $params['customer_email'] = $user->email; + } + + return $this->stripe->checkout->sessions->create($params); + } + + /* + |-------------------------------------------------------------------------- + | WEBHOOK DISPATCH + |-------------------------------------------------------------------------- + */ + + public function handleWebhook(string $payload, string $signature): void + { + $event = Webhook::constructEvent( + $payload, + $signature, + config('services.stripe.webhook_secret') + ); + + Log::info('[Stripe] Webhook received', ['type' => $event->type, 'id' => $event->id]); + + match ($event->type) { + // PrimΓ€rer Event: Subscription wurde in Stripe angelegt + 'customer.subscription.created' => $this->onSubscriptionCreated($event->data->object), + + // Fallback: falls checkout.session.completed konfiguriert ist + 'checkout.session.completed' => $this->onCheckoutCompleted($event->data->object), + + // Zahlungen (invoice.payment_succeeded absichtlich weggelassen – invoice.paid ist ausreichend) + 'invoice.paid' => $this->onInvoicePaid($event->data->object), + + 'invoice.payment_failed' => $this->onInvoiceFailed($event->data->object), + + // RΓΌckerstattungen + 'charge.refunded' => $this->onChargeRefunded($event->data->object), + + // StatusΓ€nderungen + 'customer.subscription.updated' => $this->onSubscriptionUpdated($event->data->object), + 'customer.subscription.deleted' => $this->onSubscriptionDeleted($event->data->object), + + default => null, + }; + } + + /* + |-------------------------------------------------------------------------- + | SUBSCRIPTION ERSTELLT (primΓ€rer Handler) + |-------------------------------------------------------------------------- + */ + + protected function onSubscriptionCreated(object $stripeSub): void + { + $stripeSubId = $stripeSub->id; + $stripeCustomer = is_string($stripeSub->customer) ? $stripeSub->customer : ($stripeSub->customer->id ?? null); + + $userId = $stripeSub->metadata->user_id ?? null; + $planId = $stripeSub->metadata->plan_id ?? null; + $billing = $stripeSub->metadata->billing ?? 'monthly'; + $oldStripeSubId = $stripeSub->metadata->old_stripe_sub_id ?? null; + + Log::info('[Stripe] subscription.created', compact('stripeSubId', 'userId', 'planId', 'billing')); + + // Idempotenz – bereits vorhanden + if (Subscription::where('provider_subscription_id', $stripeSubId)->where('provider', 'stripe')->exists()) { + Log::info('[Stripe] Subscription bereits vorhanden, skip', ['sub_id' => $stripeSubId]); + return; + } + + $user = User::find($userId); + $plan = Plan::find($planId); + + if (!$user || !$plan) { + Log::error('[Stripe] User oder Plan nicht gefunden', compact('userId', 'planId')); + return; + } + + $price = $billing === 'yearly' ? $plan->getYearlyPrice() : $plan->getMonthlyPrice(); + $startsAt = !empty($stripeSub->current_period_start) + ? Carbon::createFromTimestamp($stripeSub->current_period_start) + : now(); + + $endsAt = !empty($stripeSub->current_period_end) + ? Carbon::createFromTimestamp($stripeSub->current_period_end) + : now()->addMonth(); + + // Alte Stripe-Subscription kΓΌndigen – nur wenn sie in Stripe noch existiert (nicht bereits + // via cancelUserSubscription o.Γ€. beendet) und eine andere Sub-ID ist als die neue. + if ($oldStripeSubId && $oldStripeSubId !== $stripeSubId) { + try { + $old = $this->stripe->subscriptions->retrieve($oldStripeSubId); + if (!in_array($old->status, ['canceled', 'incomplete_expired'])) { + $this->stripe->subscriptions->cancel($oldStripeSubId); + Log::info('[Stripe] Alte Sub gekΓΌndigt', ['old_sub' => $oldStripeSubId]); + } + } catch (\Throwable $e) { + Log::warning('[Stripe] Alte Sub konnte nicht gecancelt werden (evtl. bereits weg)', ['old_sub' => $oldStripeSubId, 'error' => $e->getMessage()]); + } + } + + // Einzige Subscription des Users aktualisieren (eine Zeile pro User) + $subscription = Subscription::updateOrCreate( + ['user_id' => $user->id], + [ + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'plan_slug' => $plan->plan_key, + 'price' => $price, + 'original_price' => $price, + 'interval' => $billing, + 'status' => SubscriptionStatus::Active->value, + 'starts_at' => $startsAt, + 'ends_at' => $endsAt, + 'provider' => 'stripe', + 'provider_customer_id' => $stripeCustomer, + 'provider_subscription_id' => $stripeSubId, + ] + ); + + Log::info('[Stripe] Subscription angelegt/geupdated', ['sub_id' => $stripeSubId, 'plan' => $plan->name, 'user' => $user->email]); + + // Catch-up: falls invoice.paid vor subscription.created ankam β†’ Invoice direkt verarbeiten + $latestInvoiceId = is_string($stripeSub->latest_invoice ?? null) + ? $stripeSub->latest_invoice + : ($stripeSub->latest_invoice->id ?? null); + + if ($latestInvoiceId) { + $this->syncInvoicePayment($subscription, $latestInvoiceId); + } + } + + /* + |-------------------------------------------------------------------------- + | CHECKOUT SESSION COMPLETED (Fallback/ErgΓ€nzung) + |-------------------------------------------------------------------------- + */ + + protected function onCheckoutCompleted(object $session): void + { + if (($session->payment_status ?? '') !== 'paid') { + return; + } + + $newStripeSubId = is_string($session->subscription) + ? $session->subscription + : ($session->subscription->id ?? null); + + if (!$newStripeSubId) { + return; + } + + $userId = $session->client_reference_id; + $planId = $session->metadata->plan_id ?? null; + $billing = $session->metadata->billing ?? 'monthly'; + + $user = User::find($userId); + $plan = Plan::find($planId); + + if (!$user || !$plan) { + Log::error('[Stripe] checkout.completed – User/Plan nicht gefunden', compact('userId', 'planId')); + return; + } + + // Race-Condition-sicherer Check + Update der einzigen User-Subscription in einer DB-Transaktion + $subscription = DB::transaction(function () use ($user, $plan, $billing, $newStripeSubId, $session) { + // Nochmal prΓΌfen (mit Lock) ob subscription.created bereits fertig war + $existing = Subscription::where('user_id', $user->id) + ->where('provider_subscription_id', $newStripeSubId) + ->lockForUpdate() + ->first(); + + if ($existing) { + Log::info('[Stripe] checkout.completed – Subscription bereits vorhanden (Lock)', ['sub_id' => $newStripeSubId]); + return $existing; + } + + // Fallback: subscription.created hat nicht gefeuert β†’ einzige User-Subscription updaten + $stripeSub = $this->stripe->subscriptions->retrieve($newStripeSubId); + $stripeCustomer = is_string($session->customer) ? $session->customer : ($session->customer->id ?? null); + $price = $billing === 'yearly' ? $plan->getYearlyPrice() : $plan->getMonthlyPrice(); + + $sub = Subscription::updateOrCreate( + ['user_id' => $user->id], + [ + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'plan_slug' => $plan->plan_key, + 'price' => $price, + 'original_price' => $price, + 'interval' => $billing, + 'status' => SubscriptionStatus::Active->value, + 'starts_at' => !empty($stripeSub->current_period_start) ? Carbon::createFromTimestamp($stripeSub->current_period_start) : now(), + 'ends_at' => !empty($stripeSub->current_period_end) ? Carbon::createFromTimestamp($stripeSub->current_period_end) : now()->addMonth(), + 'provider' => 'stripe', + 'provider_customer_id' => $stripeCustomer, + 'provider_subscription_id' => $newStripeSubId, + ] + ); + + Log::info('[Stripe] Subscription via checkout.completed geupdated (Fallback)', ['sub_id' => $newStripeSubId]); + return $sub; + }); + + // Payment-Catch-up: in beiden FΓ€llen sicherstellen dass Payment existiert + $latestInvoiceId = $this->extractInvoiceIdFromSession($session); + if ($latestInvoiceId && $subscription) { + $this->syncInvoicePayment($subscription, $latestInvoiceId); + } + } + + protected function extractInvoiceIdFromSession(object $session): ?string + { + $inv = $session->invoice ?? null; + if (is_string($inv) && $inv !== '') return $inv; + if (is_object($inv) && !empty($inv->id)) return $inv->id; + return null; + } + + /* + |-------------------------------------------------------------------------- + | INVOICE PAID β†’ Payment anlegen + |-------------------------------------------------------------------------- + */ + + protected function onInvoicePaid(object $invoice): void + { + $invoiceId = $invoice->id; + + // Subscription-ID aus dem Invoice-Objekt extrahieren + // Stripe API < 2025: $invoice->subscription (String) + // Stripe API 2025+: $invoice->parent->subscription_details->subscription + $sub = $invoice->subscription ?? null; + $stripeSubId = match (true) { + is_string($sub) && $sub !== '' => $sub, + is_object($sub) && !empty($sub->id) => $sub->id, + default => null, + }; + + // Fallback fΓΌr Stripe API 2025+ (parent.subscription_details.subscription) + if (!$stripeSubId) { + $stripeSubId = $invoice->parent->subscription_details->subscription + ?? $invoice->parent->subscription_id + ?? null; + } + + // Fallback: erste Line-Item enthΓ€lt subscription (Γ€ltere API-Pfade) + if (!$stripeSubId && !empty($invoice->lines->data)) { + $line = $invoice->lines->data[0] ?? null; + $stripeSubId = $line->subscription ?? ($line->parent->subscription_item_details->subscription ?? null); + } + + Log::info('[Stripe] onInvoicePaid', [ + 'invoice_id' => $invoiceId, + 'sub_id_raw' => is_object($sub) ? '(object)' : $sub, + 'sub_id' => $stripeSubId, + 'billing_reason' => $invoice->billing_reason ?? null, + 'amount_paid' => $invoice->amount_paid ?? null, + ]); + + if (!$stripeSubId) { + Log::warning('[Stripe] onInvoicePaid – keine Subscription-ID im Invoice', ['invoice_id' => $invoiceId]); + return; + } + + $subscription = Subscription::where('provider_subscription_id', $stripeSubId) + ->where('provider', 'stripe') + ->first(); + + if (!$subscription) { + Log::warning('[Stripe] onInvoicePaid – Subscription nicht in DB (Race-Condition, Catch-up via subscription.created)', [ + 'sub_id' => $stripeSubId, + 'billing_reason' => $invoice->billing_reason ?? null, + ]); + return; + } + + // Lesbares billing_reason fΓΌr DB (Standard: Stripe-Wert ΓΌbernehmen) + $billingReasonDb = $invoice->billing_reason ?? null; + + // Bei VerlΓ€ngerungen / Upgrades (nicht Erstabrechnung) Laufzeit updaten + if (($invoice->billing_reason ?? '') !== 'subscription_create') { + $stripeSub = $this->stripe->subscriptions->retrieve($stripeSubId); + $updates = ['status' => SubscriptionStatus::Active->value]; + if (!empty($stripeSub->current_period_end)) { + $updates['ends_at'] = Carbon::createFromTimestamp($stripeSub->current_period_end); + } + $subscription->update($updates); + + // FΓΌr Planwechsel: "upgrade:Pro" / "downgrade:Basic" als beschreibendes Label speichern + if (($invoice->billing_reason ?? '') === 'subscription_update') { + $action = $stripeSub->metadata->action ?? null; + $toPlan = $stripeSub->metadata->to_plan ?? null; + if ($action && $toPlan) { + $billingReasonDb = $action . ':' . $toPlan; + } + } + } + + // Duplikat-Schutz via Invoice-ID (soft-check + DB-unique als harte Schranke) + if (Payment::where('provider_payment_id', $invoiceId)->where('provider', 'stripe')->exists()) { + Log::info('[Stripe] Invoice bereits erfasst, skip', ['invoice_id' => $invoiceId]); + return; + } + + try { + Payment::create([ + 'user_id' => $subscription->user_id, + 'subscription_id' => $subscription->id, + 'amount' => $invoice->amount_paid, + 'currency' => strtoupper($invoice->currency), + 'status' => PaymentStatus::Paid->value, + 'provider' => 'stripe', + 'provider_payment_id' => $invoiceId, + 'invoice_id' => $invoiceId, + 'billing_reason' => $billingReasonDb, + 'paid_at' => now(), + ]); + Log::info('[Stripe] Payment angelegt', ['invoice_id' => $invoiceId, 'amount' => $invoice->amount_paid, 'reason' => $billingReasonDb]); + } catch (\Illuminate\Database\UniqueConstraintViolationException $e) { + Log::info('[Stripe] Payment-Duplikat abgefangen (concurrent webhook)', ['invoice_id' => $invoiceId]); + } + } + + /* + |-------------------------------------------------------------------------- + | INVOICE FAILED + |-------------------------------------------------------------------------- + */ + + protected function onInvoiceFailed(object $invoice): void + { + $stripeSubId = is_string($invoice->subscription) ? $invoice->subscription : ($invoice->subscription->id ?? null); + if (!$stripeSubId) return; + + $subscription = Subscription::where('provider_subscription_id', $stripeSubId) + ->where('provider', 'stripe') + ->first(); + + if (!$subscription) return; + + $subscription->update(['status' => SubscriptionStatus::PastDue->value]); + + if (!Payment::where('provider_payment_id', $invoice->id)->where('provider', 'stripe')->exists()) { + Payment::create([ + 'user_id' => $subscription->user_id, + 'subscription_id' => $subscription->id, + 'amount' => $invoice->amount_due, + 'currency' => strtoupper($invoice->currency), + 'status' => PaymentStatus::Failed->value, + 'provider' => 'stripe', + 'provider_payment_id' => $invoice->id, + 'invoice_id' => $invoice->id, + 'billing_reason' => $invoice->billing_reason, + 'paid_at' => null, + ]); + } + } + + /* + |-------------------------------------------------------------------------- + | INVOICE SYNC (Catch-up nach subscription.created) + |-------------------------------------------------------------------------- + */ + + protected function syncInvoicePayment(Subscription $subscription, string $invoiceId): void + { + // Schon erfasst? + if (Payment::where('provider_payment_id', $invoiceId)->where('provider', 'stripe')->exists()) { + Log::info('[Stripe] syncInvoicePayment – Invoice bereits vorhanden, skip', ['invoice_id' => $invoiceId]); + return; + } + + try { + $invoice = $this->stripe->invoices->retrieve($invoiceId); + + if ($invoice->status !== 'paid') { + Log::info('[Stripe] syncInvoicePayment – Invoice nicht paid, skip', ['invoice_id' => $invoiceId, 'status' => $invoice->status]); + return; + } + + try { + Payment::create([ + 'user_id' => $subscription->user_id, + 'subscription_id' => $subscription->id, + 'amount' => $invoice->amount_paid, + 'currency' => strtoupper($invoice->currency), + 'status' => PaymentStatus::Paid->value, + 'provider' => 'stripe', + 'provider_payment_id' => $invoice->id, + 'invoice_id' => $invoice->id, + 'billing_reason' => $invoice->billing_reason, + 'paid_at' => now(), + ]); + Log::info('[Stripe] Payment via Subscription-Sync angelegt', ['invoice_id' => $invoiceId, 'amount' => $invoice->amount_paid]); + } catch (\Illuminate\Database\UniqueConstraintViolationException $e) { + Log::info('[Stripe] syncInvoicePayment – Duplikat abgefangen', ['invoice_id' => $invoiceId]); + } + } catch (\Throwable $e) { + Log::warning('[Stripe] syncInvoicePayment fehlgeschlagen', ['invoice_id' => $invoiceId, 'error' => $e->getMessage()]); + } + } + + /* + |-------------------------------------------------------------------------- + | CHARGE REFUNDED + |-------------------------------------------------------------------------- + */ + + protected function onChargeRefunded(object $charge): void + { + $chargeId = $charge->id; + $invoiceId = is_string($charge->invoice ?? null) ? $charge->invoice : ($charge->invoice->id ?? null); + + Log::info('[Stripe] charge.refunded', [ + 'charge_id' => $chargeId, + 'invoice_id' => $invoiceId, + 'fully_refunded' => $charge->refunded ?? false, + 'amount_refunded' => $charge->amount_refunded ?? 0, + ]); + + // Payment via Invoice-ID finden + $payment = $invoiceId + ? Payment::where('provider_payment_id', $invoiceId)->where('provider', 'stripe')->first() + : null; + + if (!$payment) { + Log::warning('[Stripe] charge.refunded – kein Payment gefunden', ['charge_id' => $chargeId, 'invoice_id' => $invoiceId]); + return; + } + + // PrΓΌfen ob dieser Refund ein Downgrade-Refund war (bereits als eigener Eintrag erfasst) + $latestRefundId = $charge->refunds->data[0]->id ?? null; + $isDowngradeRefund = $latestRefundId + && Payment::where('provider_payment_id', $latestRefundId)->where('provider', 'stripe')->exists(); + + if ($isDowngradeRefund) { + // Refund-Eintrag existiert bereits – Original-Payment unverΓ€ndert lassen + Log::info('[Stripe] charge.refunded – Downgrade-Refund bereits erfasst, kein Update des Original-Payments', [ + 'refund_id' => $latestRefundId, + ]); + return; + } + + // Kein bekannter Refund β†’ Original-Payment als erstattet markieren + $payment->update(['status' => PaymentStatus::Refunded->value]); + Log::info('[Stripe] Payment als erstattet markiert', ['payment_id' => $payment->id]); + + // Bei vollstΓ€ndiger Erstattung von außen (z.B. Stripe-Dashboard) Subscription kΓΌndigen + if ($charge->refunded === true) { + $subscription = Subscription::find($payment->subscription_id); + + if ($subscription) { + $subscription->update(['status' => SubscriptionStatus::Canceled->value]); + Log::info('[Stripe] Subscription wegen Vollerstattung gekΓΌndigt', ['subscription_id' => $subscription->id]); + } + } + } + + /* + |-------------------------------------------------------------------------- + | SUBSCRIPTION UPDATED / DELETED + |-------------------------------------------------------------------------- + */ + + protected function onSubscriptionUpdated(object $stripeSub): void + { + $subscription = Subscription::where('provider_subscription_id', $stripeSub->id) + ->where('provider', 'stripe') + ->first(); + + if (!$subscription) return; + + $status = match ($stripeSub->status) { + 'active' => SubscriptionStatus::Active, + 'canceled' => SubscriptionStatus::Canceled, + 'past_due' => SubscriptionStatus::PastDue, + default => SubscriptionStatus::Expired, + }; + + $updates = [ + 'status' => $status->value, + 'ends_at' => !empty($stripeSub->current_period_end) + ? Carbon::createFromTimestamp($stripeSub->current_period_end) + : $subscription->ends_at, + ]; + + // Bei Upgrade/Downgrade via API kommen Plan-Infos in den Metadata + $planId = $stripeSub->metadata->plan_id ?? null; + $billing = $stripeSub->metadata->billing ?? null; + + if ($planId) { + $plan = Plan::find($planId); + if ($plan) { + $price = $billing === 'yearly' ? $plan->getYearlyPrice() : $plan->getMonthlyPrice(); + $updates += [ + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'plan_slug' => $plan->plan_key, + 'price' => $price, + 'original_price' => $price, + 'interval' => $billing ?? $subscription->interval, + ]; + Log::info('[Stripe] Subscription Plan-Update', ['sub_id' => $stripeSub->id, 'plan' => $plan->name]); + } + } + + $subscription->update($updates); + } + + protected function onSubscriptionDeleted(object $stripeSub): void + { + $subscription = Subscription::where('provider_subscription_id', $stripeSub->id) + ->where('provider', 'stripe') + ->first(); + + if (!$subscription) return; + + // Extern gelΓΆschte Subscriptions (z. B. durch Stripe-Dashboard oder Zahlungsausfall) + // auf Free-Plan zurΓΌcksetzen – customer_id bleibt fΓΌr spΓ€teres Re-Abonnieren erhalten + $freePlan = Plan::freePlan(); + + $subscription->update([ + 'plan_id' => $freePlan?->id, + 'plan_name' => $freePlan?->name ?? 'Free', + 'plan_slug' => $freePlan?->plan_key ?? 'free', + 'price' => 0, + 'original_price' => 0, + 'interval' => 'monthly', + 'status' => SubscriptionStatus::Active->value, + 'starts_at' => now(), + 'ends_at' => null, + 'provider' => null, + 'provider_subscription_id' => null, + ]); + + Log::info('[Stripe] Subscription via webhook auf Free-Plan zurΓΌckgesetzt', ['sub_id' => $stripeSub->id]); + } + + /* + |-------------------------------------------------------------------------- + | UPGRADE / DOWNGRADE (Bestehende Stripe-Subscription updaten) + |-------------------------------------------------------------------------- + */ + + public function updateStripeSubscription(string $stripeSubId, Plan $plan, string $billing, string $userId): void + { + $currency = config('services.stripe.currency', 'eur'); + $price = $billing === 'yearly' ? $plan->getYearlyPrice() : $plan->getMonthlyPrice(); + + $stripeSub = $this->stripe->subscriptions->retrieve($stripeSubId); + $itemId = $stripeSub->items->data[0]->id ?? null; + + if (!$itemId) { + throw new \RuntimeException('Stripe subscription hat keine Items'); + } + + // Upgrade oder Downgrade ermitteln (anhand des aktuellen DB-Preises) + $currentDbPrice = Subscription::where('provider_subscription_id', $stripeSubId) + ->where('provider', 'stripe') + ->value('price') ?? 0; + $action = $price >= $currentDbPrice ? 'upgrade' : 'downgrade'; + + // subscriptions->update() erlaubt kein product_data β†’ erst Price anlegen, dann ID nutzen + $priceObj = $this->stripe->prices->create([ + 'currency' => $currency, + 'unit_amount' => $price, + 'product_data' => [ + 'name' => $plan->name, + ], + 'recurring' => [ + 'interval' => $billing === 'yearly' ? 'year' : 'month', + ], + ]); + + $this->stripe->subscriptions->update($stripeSubId, [ + 'items' => [[ + 'id' => $itemId, + 'price' => $priceObj->id, + ]], + // create_prorations: Proration-Posten werden auf die nΓ€chste Rechnung gelegt + // und dort korrekt mit Guthaben verrechnet. always_invoice wΓΌrde sofort zwei + // getrennte Transaktionen erzeugen (Kredit auf Kundenkonto + voller neuer Preis). + 'proration_behavior' => 'always_invoice', + 'metadata' => [ + 'user_id' => $userId, + 'plan_id' => $plan->id, + 'billing' => $billing, + 'action' => $action, // 'upgrade' oder 'downgrade' + 'to_plan' => $plan->name, // Ziel-Planname fΓΌr Invoice-Label + ], + ]); + + Log::info('[Stripe] Subscription per API geupdated', ['sub_id' => $stripeSubId, 'plan' => $plan->name]); + + // DB sofort aktualisieren fΓΌr direktes UI-Feedback (Webhook kommt nach) + Subscription::where('provider_subscription_id', $stripeSubId) + ->where('provider', 'stripe') + ->update([ + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'plan_slug' => $plan->plan_key, + 'price' => $price, + 'original_price' => $price, + 'interval' => $billing, + 'status' => SubscriptionStatus::Active->value, + ]); + } + + /* + |-------------------------------------------------------------------------- + | PRORATION REFUND (anteiliger RΓΌckerstattung bei Wechsel auf Free) + |-------------------------------------------------------------------------- + */ + + protected function issueProrationRefund(Subscription $sub): void + { + $stripeSubId = $sub->provider_subscription_id; + + try { + // Verbleibende Zeit aus der DB-Subscription berechnen (zuverlΓ€ssiger als Invoice-Line-Items, + // da Stripe nach always_invoice-Upgrades current_period_end temporΓ€r zurΓΌcksetzt). + $now = now(); + $endsAt = $sub->ends_at; + + if (!$endsAt || $endsAt->isPast()) { + Log::info('[Stripe] Proration-Refund – Subscription bereits abgelaufen', ['sub_id' => $stripeSubId]); + return; + } + + $startsAt = $sub->starts_at ?? $now; + $totalSeconds = max(1, $endsAt->timestamp - $startsAt->timestamp); + $remainingSeconds = $endsAt->timestamp - $now->timestamp; + + if ($remainingSeconds <= 0) { + return; + } + + // Erstattungsbetrag auf Basis des vollen Abonnementpreises (nicht invoice.amount_paid, + // da bei Upgrades nur der Differenzbetrag auf der letzten Invoice steht). + $refundAmount = (int) round(($remainingSeconds / $totalSeconds) * $sub->price); + + if ($refundAmount <= 0) { + return; + } + + $chargeId = null; + $currency = config('services.stripe.currency', 'eur'); + $customerId = $sub->provider_customer_id; + + // Strategie 1: Bezahlte Invoices der Subscription durchsuchen + $invoices = $this->stripe->invoices->all([ + 'subscription' => $stripeSubId, + 'status' => 'paid', + 'limit' => 5, + ]); + + Log::info('[Stripe] Proration-Refund – Invoices gefunden', [ + 'sub_id' => $stripeSubId, + 'count' => count($invoices->data), + ]); + + foreach ($invoices->data as $invoice) { + $currency = strtoupper($invoice->currency); + + // Stripe API < 2025: invoice.charge ist direkt gesetzt + $rawCharge = $invoice->charge ?? null; + $cid = is_string($rawCharge) ? $rawCharge : ($rawCharge->id ?? null); + + // Stripe API 2025+: charge hΓ€ngt am PaymentIntent (invoice.charge ist null) + if (!$cid) { + $rawPi = $invoice->payment_intent ?? null; + $piId = is_string($rawPi) ? $rawPi : ($rawPi->id ?? null); + if ($piId) { + try { + $pi = $this->stripe->paymentIntents->retrieve($piId); + $rawLatest = $pi->latest_charge ?? null; + $cid = is_string($rawLatest) ? $rawLatest : ($rawLatest->id ?? null); + Log::info('[Stripe] Proration-Refund – PaymentIntent lookup', [ + 'pi_id' => $piId, + 'latest_charge' => $cid ?? 'null', + ]); + } catch (\Throwable $e) { + Log::warning('[Stripe] Proration-Refund – PaymentIntent-Abruf fehlgeschlagen', ['pi_id' => $piId, 'error' => $e->getMessage()]); + } + } + } + + Log::info('[Stripe] Proration-Refund – Invoice geprΓΌft', [ + 'invoice_id' => $invoice->id, + 'charge_raw' => is_string($rawCharge) ? $rawCharge : (is_object($rawCharge) ? 'object' : 'null'), + 'charge_found' => $cid ?? 'null', + ]); + + if ($cid) { + $chargeId = $cid; + break; + } + } + + // Strategie 2 (Fallback): Charges direkt ΓΌber Customer abfragen – + // umgeht API-Versionsunterschiede im Invoice-Objekt vollstΓ€ndig. + if (!$chargeId && $customerId) { + Log::info('[Stripe] Proration-Refund – Fallback: Charges per Customer', ['customer_id' => $customerId]); + + $charges = $this->stripe->charges->all([ + 'customer' => $customerId, + 'limit' => 10, + ]); + + foreach ($charges->data as $charge) { + if ($charge->status === 'succeeded') { + $currency = strtoupper($charge->currency); + $chargeId = $charge->id; + Log::info('[Stripe] Proration-Refund – Charge via Customer gefunden', ['charge_id' => $chargeId]); + break; + } + } + } + + if (!$chargeId) { + Log::warning('[Stripe] Proration-Refund – keine erstattungsfΓ€hige Charge gefunden', ['sub_id' => $stripeSubId, 'customer_id' => $customerId]); + return; + } + + // Bereits erstatteten Betrag berΓΌcksichtigen und auf tatsΓ€chlich erstattbaren Betrag kappen + $charge = $this->stripe->charges->retrieve($chargeId); + $maxRefundable = ($charge->amount ?? 0) - ($charge->amount_refunded ?? 0); + $refundAmount = min($refundAmount, $maxRefundable); + + if ($refundAmount <= 0) { + Log::info('[Stripe] Proration-Refund – Charge bereits vollstΓ€ndig erstattet', ['charge_id' => $chargeId]); + return; + } + + $refund = $this->stripe->refunds->create([ + 'charge' => $chargeId, + 'amount' => $refundAmount, + 'reason' => 'requested_by_customer', + 'metadata' => [ + 'reason' => 'downgrade_to_free', + 'subscription_id' => $stripeSubId, + ], + ]); + + Log::info('[Stripe] Proration-Refund erstellt', [ + 'refund_id' => $refund->id, + 'amount' => $refundAmount, + 'sub_id' => $stripeSubId, + ]); + + // Eigenen Payment-Eintrag anlegen damit die Erstattung in der RechnungsΓΌbersicht sichtbar ist + try { + Payment::create([ + 'user_id' => $sub->user_id, + 'subscription_id' => $sub->id, + 'amount' => $refundAmount, + 'currency' => $currency, + 'status' => PaymentStatus::Refunded->value, + 'provider' => 'stripe', + 'provider_payment_id' => $refund->id, + 'invoice_id' => null, + 'billing_reason' => 'refund:downgrade_to_free', + 'paid_at' => now(), + ]); + } catch (\Illuminate\Database\UniqueConstraintViolationException) { + // bereits vorhanden – kein Problem + } + + } catch (\Throwable $e) { + // Refund-Fehler nicht weiterwerfen – KΓΌndigung soll trotzdem durchlaufen + Log::error('[Stripe] Proration-Refund fehlgeschlagen', ['sub_id' => $stripeSubId, 'error' => $e->getMessage()]); + } + } + + /* + |-------------------------------------------------------------------------- + | FREE PLAN (Stripe-Subscription kΓΌndigen) + |-------------------------------------------------------------------------- + */ + + public function cancelUserSubscription(User $user): void + { + $sub = Subscription::where('user_id', $user->id) + ->where('provider', 'stripe') + ->whereNotNull('provider_subscription_id') + ->whereIn('status', [SubscriptionStatus::Active->value, SubscriptionStatus::PastDue->value]) + ->latest('created_at') + ->first(); + + if (!$sub) { + Log::info('[Stripe] cancelUserSubscription – kein aktives Abo gefunden', ['user_id' => $user->id]); + return; + } + + // Anteiligen Restbetrag zurΓΌckerstatten bevor die Sub gekΓΌndigt wird + $this->issueProrationRefund($sub); + + try { + $this->stripe->subscriptions->cancel($sub->provider_subscription_id); + Log::info('[Stripe] Subscription sofort gekΓΌndigt', ['sub_id' => $sub->provider_subscription_id]); + } catch (\Throwable $e) { + Log::error('[Stripe] KΓΌndigung fehlgeschlagen', ['sub_id' => $sub->provider_subscription_id, 'error' => $e->getMessage()]); + throw $e; + } + + // Bestehende Subscription auf Free-Plan zurΓΌcksetzen – provider_customer_id bleibt erhalten! + $freePlan = Plan::freePlan(); + + $sub->update([ + 'plan_id' => $freePlan?->id, + 'plan_name' => $freePlan?->name ?? 'Free', + 'plan_slug' => $freePlan?->plan_key ?? 'free', + 'price' => 0, + 'original_price' => 0, + 'interval' => 'monthly', + 'status' => SubscriptionStatus::Active->value, + 'starts_at' => now(), + 'ends_at' => null, + 'provider' => null, + 'provider_subscription_id' => null, + // provider_customer_id absichtlich nicht ΓΌberschreiben β†’ bleibt fΓΌr spΓ€teres Re-Abonnieren + ]); + + Log::info('[Stripe] Subscription auf Free-Plan zurΓΌckgesetzt (customer_id behalten)', ['user_id' => $user->id]); + } + + /* + |-------------------------------------------------------------------------- + | BILLING PORTAL + |-------------------------------------------------------------------------- + */ + + public function createBillingPortalSession(string $stripeCustomerId): string + { + $session = $this->stripe->billingPortal->sessions->create([ + 'customer' => $stripeCustomerId, + 'return_url' => route('subscription.index'), + ]); + + return $session->url; + } +} diff --git a/src/app/Services/VerificationService.php b/src/app/Services/VerificationService.php new file mode 100644 index 0000000..801e29b --- /dev/null +++ b/src/app/Services/VerificationService.php @@ -0,0 +1,86 @@ +id) + ->where('type', $type) + ->whereNull('verified_at') + ->latest() + ->first(); + + if ($verification) { + + // πŸ”₯ UPDATE statt DELETE + $verification->update([ + 'code' => rand(100000, 999999), + 'expires_at' => now()->addMinutes(10), + ]); + + } else { + + $verification = Verification::create([ + 'user_id' => $user->id, + 'type' => $type, + 'channel' => 'email', + 'code' => rand(100000, 999999), + 'expires_at' => now()->addMinutes(10), + ]); + } + + // πŸ”₯ URL + $url = URL::temporarySignedRoute( + 'verify.user', + $verification->expires_at, + [ + 'verification' => $verification->id + ] + ); + + MailService::queue( + $user->email, + 'auth.verify', + [ + 'user' => $user->name, + 'url' => $url, + ], + $user->locale, + null, + $user->id + ); + + return $verification; + } + + + public static function verify(User $user, string $code, string $type = 'email'): bool + { + $verification = Verification::where('user_id', $user->id) + ->where('type', $type) + ->latest() + ->first(); + + if (!$verification) return false; + if ($verification->isExpired()) return false; + if ($verification->code !== $code) return false; + + $verification->update([ + 'verified_at' => now(), + ]); + + if ($type === 'email') { + $user->update([ + 'email_verified_at' => now(), + ]); + } + + return true; + } +} diff --git a/src/app/View/Components/AppHeader.php b/src/app/View/Components/AppHeader.php new file mode 100644 index 0000000..1b07e36 --- /dev/null +++ b/src/app/View/Components/AppHeader.php @@ -0,0 +1,26 @@ +handleCommand(new ArgvInput); + +exit($status); diff --git a/src/bootstrap/app.php b/src/bootstrap/app.php new file mode 100644 index 0000000..e38f90f --- /dev/null +++ b/src/bootstrap/app.php @@ -0,0 +1,55 @@ +withRouting( + commands: __DIR__.'/../routes/console.php', + channels: __DIR__.'/../routes/channels.php', + health: '/up', + then: function () { + Route::middleware(['web']) + ->domain(env('DOMAIN_APP', 'app.aziros.com')) + ->group(base_path('routes/web.php')); + Route::middleware(['api']) + ->domain(env('DOMAIN_API', 'api.aziros.com')) + ->group(base_path('routes/api.php')); + Route::middleware(['web']) + ->domain(env('DOMAIN_CONNECT', 'connect.aziros.com')) + ->group(base_path('routes/connect.php')); + Route::domain(env('DOMAIN_WWW', 'www.aziros.com')) + ->group(base_path('routes/www.php')); + + } + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->validateCsrfTokens(except: [ + 'stripe/webhook', + 'webhooks/google-calendar', + ]); + $middleware->web([ + \App\Http\Middleware\SetLocale::class, + \App\Http\Middleware\TrackAffiliateCode::class, + \App\Http\Middleware\CheckAppVersion::class, + ]); + $middleware->api([ + \App\Http\Middleware\ForceJsonResponse::class, + ]); + $middleware->alias([ + 'auth.custom' => \App\Http\Middleware\EnsureAuthenticated::class, + 'guest.custom' => \App\Http\Middleware\RedirectIfAuthenticatedCustom::class, + 'user' => \App\Http\Middleware\EnsureUserIsVerified::class, + 'admin' => \App\Http\Middleware\RoleMiddleware::class, + 'role' => \App\Http\Middleware\RoleMiddleware::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->render(function (InvalidSignatureException $e, $request) { + return redirect()->route('verify.notice', [ + 'user' => $request->route('user') + ])->with('error.auth.verify.link_expired', t('auth.verify.link_expired')); + }); + })->create(); diff --git a/src/bootstrap/providers.php b/src/bootstrap/providers.php new file mode 100644 index 0000000..fc94ae6 --- /dev/null +++ b/src/bootstrap/providers.php @@ -0,0 +1,7 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "clue/redis-protocol", + "version": "v0.3.2", + "source": { + "type": "git", + "url": "https://github.com/clue/redis-protocol.git", + "reference": "6f565332f5531b7722d1e9c445314b91862f6d6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/redis-protocol/zipball/6f565332f5531b7722d1e9c445314b91862f6d6c", + "reference": "6f565332f5531b7722d1e9c445314b91862f6d6c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\Redis\\Protocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@lueck.tv" + } + ], + "description": "A streaming Redis protocol (RESP) parser and serializer written in pure PHP.", + "homepage": "https://github.com/clue/redis-protocol", + "keywords": [ + "parser", + "protocol", + "redis", + "resp", + "serializer", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/redis-protocol/issues", + "source": "https://github.com/clue/redis-protocol/tree/v0.3.2" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2024-08-07T11:06:28+00:00" + }, + { + "name": "clue/redis-react", + "version": "v2.8.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-redis.git", + "reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-redis/zipball/84569198dfd5564977d2ae6a32de4beb5a24bdca", + "reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca", + "shasum": "" + }, + "require": { + "clue/redis-protocol": "^0.3.2", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.0 || ^1.1", + "react/promise-timer": "^1.11", + "react/socket": "^1.16" + }, + "require-dev": { + "clue/block-react": "^1.5", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\Redis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering" + } + ], + "description": "Async Redis client implementation, built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-redis", + "keywords": [ + "async", + "client", + "database", + "reactphp", + "redis" + ], + "support": { + "issues": "https://github.com/clue/reactphp-redis/issues", + "source": "https://github.com/clue/reactphp-redis/tree/v2.8.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2025-01-03T16:18:33+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Γ‰vΓ©nement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "MΓ‘rk SΓ‘gi-KazΓ‘r", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "MΓ‘rk SΓ‘gi-KazΓ‘r", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "MΓ‘rk SΓ‘gi-KazΓ‘r", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-03-10T16:41:02+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v13.3.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "118b7063c44a2f3421d1646f5ddf08defcfd1db3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/118b7063c44a2f3421d1646f5ddf08defcfd1db3", + "reference": "118b7063c44a2f3421d1646f5ddf08defcfd1db3", + "shasum": "" + }, + "require": { + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1 || 2.0", + "psr/log-implementation": "1.0 || 2.0 || 3.0", + "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^11.0.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "^2.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-04-01T15:39:53+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.16", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/11e7d5f93803a2190b00e145142cb00a33d17ad2", + "reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.16" + }, + "time": "2026-03-23T14:35:33+00:00" + }, + { + "name": "laravel/reverb", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/reverb.git", + "reference": "a9c2b24ba455d0b2c22bb2851c15ba1adcb75240" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/reverb/zipball/a9c2b24ba455d0b2c22bb2851c15ba1adcb75240", + "reference": "a9c2b24ba455d0b2c22bb2851c15ba1adcb75240", + "shasum": "" + }, + "require": { + "clue/redis-react": "^2.6", + "guzzlehttp/psr7": "^2.6", + "illuminate/console": "^10.47|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.47|^11.0|^12.0|^13.0", + "illuminate/http": "^10.47|^11.0|^12.0|^13.0", + "illuminate/support": "^10.47|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.15|^0.2.0|^0.3.0", + "php": "^8.2", + "pusher/pusher-php-server": "^7.2", + "ratchet/rfc6455": "^0.4", + "react/promise-timer": "^1.10", + "react/socket": "^1.14", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-foundation": "^6.3|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "phpstan/phpstan": "^1.10", + "ratchet/pawl": "^0.4.1", + "react/async": "^4.2", + "react/http": "^1.9" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Reverb\\ApplicationManagerServiceProvider", + "Laravel\\Reverb\\ReverbServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Reverb\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Joe Dixon", + "email": "joe@laravel.com" + } + ], + "description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.", + "keywords": [ + "WebSockets", + "laravel", + "real-time", + "websocket" + ], + "support": { + "issues": "https://github.com/laravel/reverb/issues", + "source": "https://github.com/laravel/reverb/tree/v1.10.0" + }, + "time": "2026-03-29T14:51:57+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-02-20T19:59:49+00:00" + }, + { + "name": "laravel/tinker", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "cc74081282ba2e3dae1f0068ccb330370d24634e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/cc74081282ba2e3dae1f0068ccb330370d24634e", + "reference": "cc74081282ba2e3dae1f0068ccb330370d24634e", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.0" + }, + "time": "2026-03-17T14:53:17+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.33.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "570b8871e0ce693764434b29154c54b434905350" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/570b8871e0ce693764434b29154c54b434905350", + "reference": "570b8871e0ce693764434b29154c54b434905350", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.33.0" + }, + "time": "2026-03-25T07:59:30+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "livewire/livewire", + "version": "v4.2.4", + "source": { + "type": "git", + "url": "https://github.com/livewire/livewire.git", + "reference": "7d0bfa46269b1ec186b8cdd38baffee5cc647d10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/livewire/zipball/7d0bfa46269b1ec186b8cdd38baffee5cc647d10", + "reference": "7d0bfa46269b1ec186b8cdd38baffee5cc647d10", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/routing": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", + "league/mime-type-detection": "^1.9", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-kernel": "^6.2|^7.0|^8.0" + }, + "require-dev": { + "calebporzio/sushi": "^2.1", + "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.3.1", + "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0", + "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.5", + "psy/psysh": "^0.11.22|^0.12" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Livewire": "Livewire\\Livewire" + }, + "providers": [ + "Livewire\\LivewireServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "A front-end framework for Laravel.", + "support": { + "issues": "https://github.com/livewire/livewire/issues", + "source": "https://github.com/livewire/livewire/tree/v4.2.4" + }, + "funding": [ + { + "url": "https://github.com/livewire", + "type": "github" + } + ], + "time": "2026-04-02T20:48:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-03-11T17:23:39+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "πŸ“ Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "πŸ›  Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.3" + }, + "time": "2026-02-13T03:05:33+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4714da6efdc782c06690bc72ce34fae7941c2d9f", + "reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-64bit": "*" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^7|^8|^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "suggest": { + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "ParagonIE\\Sodium\\": "namespaced/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0" + }, + "time": "2025-12-30T16:12:18+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.22", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.22" + }, + "time": "2026-03-22T23:03:24+00:00" + }, + { + "name": "pusher/pusher-php-server", + "version": "7.2.7", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.2", + "paragonie/sodium_compat": "^1.6|^2.0", + "php": "^7.3|^8.0", + "psr/log": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "overtrue/phplint": "^2.3", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Pusher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Library for interacting with the Pusher REST API", + "keywords": [ + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" + ], + "support": { + "issues": "https://github.com/pusher/pusher-http-php/issues", + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.7" + }, + "time": "2025-01-06T10:56:20+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "ratchet/rfc6455", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "psr/http-factory-implementation": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.7", + "phpunit/phpunit": "^9.5", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" + }, + "time": "2025-02-24T01:18:22+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\Timer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:27:45+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian LΓΌck", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.93.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-02-21T12:49:54+00:00" + }, + { + "name": "stripe/stripe-php", + "version": "v20.0.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "7338bd140e641b1f9c7cb602e2de971e14db6b3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/7338bd140e641b1f9c7cb602e2de971e14db6b3b", + "reference": "7338bd140e641b1f9c7cb602e2de971e14db6b3b", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=7.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.94.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "files": [ + "lib/version_check.php" + ], + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v20.0.0" + }, + "time": "2026-03-26T01:57:48+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:54:39+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "b055f228a4178a1d6774909903905e3475f3eac8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b055f228a4178a1d6774909903905e3475f3eac8", + "reference": "b055f228a4178a1d6774909903905e3475f3eac8", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-FranΓ§ois Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "f57b899fa736fd71121168ef268f23c206083f0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a", + "reference": "f57b899fa736fd71121168ef268f23c206083f0a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:54:39+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e0be088d22278583a82da281886e8c3592fbf149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "017e76ad089bac281553389269e259e155935e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/017e76ad089bac281553389269e259e155935e1a", + "reference": "017e76ad089bac281553389269e259e155935e1a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-31T20:57:01+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f6ea532250b476bfc1b56699b388a1bdbf168f62", + "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/6df02f99998081032da3407a8d6c4e1dcb5d4379", + "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T14:11:46+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "GrΓ©goire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b", + "reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "114ac57257d75df748eda23dd003878080b8e688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688", + "reference": "114ac57257d75df748eda23dd003878080b8e688", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "33600f8489485425bfcddd0d983391038d3422e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7", + "reference": "33600f8489485425bfcddd0d983391038d3422e7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/6883ebdf7bf6a12b37519dbc0df62b0222401b56", + "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "GrΓ©goire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:44:50+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "wire-elements/modal", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/wire-elements/modal.git", + "reference": "3c17ed4e5d1506773db37dbbfdcacff12b037317" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wire-elements/modal/zipball/3c17ed4e5d1506773db37dbbfdcacff12b037317", + "reference": "3c17ed4e5d1506773db37dbbfdcacff12b037317", + "shasum": "" + }, + "require": { + "livewire/livewire": "^3.2.3|^4.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.9" + }, + "require-dev": { + "orchestra/testbench": "^8.5", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LivewireUI\\Modal\\LivewireModalServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "LivewireUI\\Modal\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Philo Hermans", + "email": "me@philohermans.com" + } + ], + "description": "Laravel Livewire modal component", + "keywords": [ + "laravel", + "livewire", + "modal" + ], + "support": { + "issues": "https://github.com/wire-elements/modal/issues", + "source": "https://github.com/wire-elements/modal/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/PhiloNL", + "type": "github" + } + ], + "time": "2026-01-23T11:08:48+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "FranΓ§ois Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-02-09T13:44:54+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-03-12T15:51:39+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "PΓ‘draic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.2", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "6eb16883e74fd725ac64dbe81544c961ab448ba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/6eb16883e74fd725ac64dbe81544c961ab448ba5", + "reference": "6eb16883e74fd725ac64dbe81544c961ab448ba5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.4" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.3", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.2.0", + "laravel/pint": "^1.29.0", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.0.0", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-03-31T21:51:27+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-02-06T06:01:44+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.15", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "aeb6899ffdbbf4b4ff5e6b6ebb77b35c51bb6d9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/aeb6899ffdbbf4b4ff5e6b6ebb77b35c51bb6d9a", + "reference": "aeb6899ffdbbf4b4ff5e6b6ebb77b35c51bb6d9a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.4", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.15" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-03-31T06:41:33+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2025-09-14T09:36:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:28:48+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-03-15T07:05:40+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/src/config/ai_models.php b/src/config/ai_models.php new file mode 100644 index 0000000..079c510 --- /dev/null +++ b/src/config/ai_models.php @@ -0,0 +1,30 @@ + [ + 'label' => 'GPT-4o Mini (Free)', + 'model' => 'gpt-4o-mini', + 'input_cost' => 0.00015, + 'output_cost' => 0.0006, + 'max_tokens' => 800, + 'temperature' => 0.2, + ], + + 'gpt-4o' => [ + 'label' => 'GPT-4o (Pro)', + 'model' => 'gpt-4o', + 'input_cost' => 0.005, + 'output_cost' => 0.015, + 'max_tokens' => 2000, + 'temperature' => 0.2, + ], + + 'gpt-4-1' => [ + 'label' => 'GPT-4.1 (Premium)', + 'model' => 'gpt-4.1', + 'input_cost' => 0.01, + 'output_cost' => 0.03, + 'max_tokens' => 4000, + 'temperature' => 0.2, + ], +]; diff --git a/src/config/app.php b/src/config/app.php new file mode 100644 index 0000000..490270d --- /dev/null +++ b/src/config/app.php @@ -0,0 +1,138 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + 'api_url' => env('APP_CONNECT_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'de'), + + 'locales' => [ + 'de' => 'Deutsch', + 'en' => 'English', + ], + + 'default_locale' => 'de', + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'de'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + + + + 'version' => env('APP_VERSION', '1.0.0'), + +]; diff --git a/src/config/auth.php b/src/config/auth.php new file mode 100644 index 0000000..d7568ff --- /dev/null +++ b/src/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/src/config/automations.php b/src/config/automations.php new file mode 100644 index 0000000..0106221 --- /dev/null +++ b/src/config/automations.php @@ -0,0 +1,133 @@ + [ + 'daily_agenda', + 'birthday_reminder', + ], + + 'types' => [ + 'event_reminder' => [ + 'name' => 'Termin-Erinnerung', + 'description' => 'Erinnert dich X Minuten/Stunden vor jedem Termin per Benachrichtigung oder E-Mail.', + 'icon' => 'bell', + 'icon_app' => 'notifications-outline', + 'color' => 'indigo', + 'color_hex' => '#4F46E5', + 'is_free' => false, + 'channels' => ['push'], + 'defaults' => [ + 'minutes_before' => 30, + ], + ], + 'daily_agenda' => [ + 'name' => 'TΓ€gliche Agenda', + 'description' => 'Sendet dir jeden Morgen eine Übersicht aller Termine des Tages.', + 'icon' => 'sun', + 'icon_app' => 'sunny-outline', + 'color' => 'amber', + 'color_hex' => '#F59E0B', + 'is_free' => true, + 'channels' => ['push', 'email'], + 'defaults' => [ + 'send_time' => '08:00', + 'weekdays_only' => false, + ], + ], + 'weekly_overview' => [ + 'name' => 'WΓΆchentliche Vorschau', + 'description' => 'Schickt dir jeden Montag eine Zusammenfassung aller Termine der kommenden Woche.', + 'icon' => 'calendar-days', + 'icon_app' => 'calendar-outline', + 'color' => 'blue', + 'color_hex' => '#0EA5E9', + 'is_free' => false, + 'channels' => ['email'], + 'defaults' => [ + 'send_day' => 1, + 'send_time' => '08:00', + ], + ], + 'event_followup' => [ + 'name' => 'Termin-Nachbereitung', + 'description' => 'Erstellt nach einem Termin automatisch eine AktivitΓ€t zur Nachverfolgung.', + 'icon' => 'clipboard-document-check', + 'icon_app' => 'clipboard-outline', + 'color' => 'green', + 'color_hex' => '#10B981', + 'is_free' => false, + 'channels' => ['push'], + 'defaults' => [ + 'delay_minutes' => 30, + ], + ], + 'birthday_reminder' => [ + 'name' => 'Geburtstags-Erinnerung', + 'description' => 'Erinnert dich X Tage vor dem Geburtstag deiner Kontakte.', + 'icon' => 'cake', + 'icon_app' => 'gift-outline', + 'color' => 'pink', + 'color_hex' => '#EC4899', + 'is_free' => true, + 'channels' => ['push'], + 'defaults' => [ + 'days_before' => 7, + ], + ], + 'no_activity_reminder' => [ + 'name' => 'Leere-Tage-Erinnerung', + 'description' => 'Benachrichtigt dich, wenn du X aufeinanderfolgende Tage ohne Termine hattest.', + 'icon' => 'clock', + 'icon_app' => 'time-outline', + 'color' => 'gray', + 'color_hex' => '#6B7280', + 'is_free' => false, + 'channels' => ['push'], + 'defaults' => [ + 'days_inactive' => 3, + ], + ], + 'daily_summary' => [ + 'name' => 'TagesrΓΌckblick', + 'description' => 'Sendet dir abends eine Zusammenfassung aller erledigten Termine und AktivitΓ€ten des Tages.', + 'icon' => 'moon', + 'icon_app' => 'moon-outline', + 'color' => 'violet', + 'color_hex' => '#8B5CF6', + 'is_free' => false, + 'channels' => ['email'], + 'defaults' => [ + 'send_time' => '18:00', + ], + ], + 'contact_followup' => [ + 'name' => 'Kontakt-Nachfassung', + 'description' => 'Erinnert dich, wenn du einen Kontakt seit X Tagen nicht mehr kontaktiert hast.', + 'icon' => 'user-group', + 'icon_app' => 'people-outline', + 'color' => 'orange', + 'color_hex' => '#F97316', + 'is_free' => false, + 'channels' => ['push'], + 'defaults' => [ + 'days_since_contact' => 14, + ], + ], + 'free_slots_report' => [ + 'name' => 'Freie-Zeiten-Bericht', + 'description' => 'Schickt dir wΓΆchentlich eine Übersicht deiner freien Zeitfenster fΓΌr die kommende Woche.', + 'icon' => 'calendar', + 'icon_app' => 'stats-chart-outline', + 'color' => 'teal', + 'color_hex' => '#14B8A6', + 'is_free' => false, + 'channels' => ['email'], + 'defaults' => [ + 'send_day' => 7, + 'send_time' => '17:00', + 'min_slot_minutes' => 60, + ], + ], + ], +]; diff --git a/src/config/broadcasting.php b/src/config/broadcasting.php new file mode 100644 index 0000000..ebc3fb9 --- /dev/null +++ b/src/config/broadcasting.php @@ -0,0 +1,82 @@ + env('BROADCAST_CONNECTION', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over WebSockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'reverb' => [ + 'driver' => 'reverb', + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), + 'encrypted' => true, + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/src/config/cache.php b/src/config/cache.php new file mode 100644 index 0000000..79f3bad --- /dev/null +++ b/src/config/cache.php @@ -0,0 +1,130 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), +// 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel'))), + + /* + |-------------------------------------------------------------------------- + | Serializable Classes + |-------------------------------------------------------------------------- + | + | This value determines the classes that can be unserialized from cache + | storage. By default, no PHP classes will be unserialized from your + | cache to prevent gadget chain attacks if your APP_KEY is leaked. + | + */ + + 'serializable_classes' => false, + +]; diff --git a/src/config/database.php b/src/config/database.php new file mode 100644 index 0000000..b5d61e7 --- /dev/null +++ b/src/config/database.php @@ -0,0 +1,184 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + +// 'options' => [ +// 'cluster' => env('REDIS_CLUSTER', 'redis'), +// 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), +// 'persistent' => env('REDIS_PERSISTENT', false), +// ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/src/config/filesystems.php b/src/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/src/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/src/config/livewire.php b/src/config/livewire.php new file mode 100644 index 0000000..1211ffa --- /dev/null +++ b/src/config/livewire.php @@ -0,0 +1,282 @@ + [ + resource_path('views/components'), + resource_path('views/livewire'), + ], + + /* + |--------------------------------------------------------------------------- + | Component Namespaces + |--------------------------------------------------------------------------- + | + | This value sets default namespaces that will be used to resolve view-based + | components like single-file and multi-file components. These folders'll + | also be referenced when creating new components via the make command. + | + */ + + 'component_namespaces' => [ + 'layouts' => resource_path('views/layouts'), + 'pages' => resource_path('views/pages'), + ], + + /* + |--------------------------------------------------------------------------- + | Page Layout + |--------------------------------------------------------------------------- + | The view that will be used as the layout when rendering a single component as + | an entire page via `Route::livewire('/post/create', 'pages::create-post')`. + | In this case, the content of pages::create-post will render into $slot. + | + */ + + 'component_layout' => 'layouts::app', + + /* + |--------------------------------------------------------------------------- + | Lazy Loading Placeholder + |--------------------------------------------------------------------------- + | Livewire allows you to lazy load components that would otherwise slow down + | the initial page load. Every component can have a custom placeholder or + | you can define the default placeholder view for all components below. + | + */ + + 'component_placeholder' => null, // Example: 'placeholders::skeleton' + + /* + |--------------------------------------------------------------------------- + | Make Command + |--------------------------------------------------------------------------- + | This value determines the default configuration for the artisan make command + | You can configure the component type (sfc, mfc, class) and whether to use + | the high-voltage (⚑) emoji as a prefix in the sfc|mfc component names. + | + */ + + 'make_command' => [ + 'type' => 'class', // Options: 'sfc', 'mfc', 'class' + 'emoji' => true, // Options: true, false + 'with' => [ + 'js' => false, + 'css' => false, + 'test' => false, + ], + ], + + /* + |--------------------------------------------------------------------------- + | Class Namespace + |--------------------------------------------------------------------------- + | + | This value sets the root class namespace for Livewire component classes in + | your application. This value will change where component auto-discovery + | finds components. It's also referenced by the file creation commands. + | + */ + + 'class_namespace' => 'App\\Livewire', + + /* + |--------------------------------------------------------------------------- + | Class Path + |--------------------------------------------------------------------------- + | + | This value is used to specify the path where Livewire component class files + | are created when running creation commands like `artisan make:livewire`. + | This path is customizable to match your projects directory structure. + | + */ + + 'class_path' => app_path('Livewire'), + + /* + |--------------------------------------------------------------------------- + | View Path + |--------------------------------------------------------------------------- + | + | This value is used to specify where Livewire component Blade templates are + | stored when running file creation commands like `artisan make:livewire`. + | It is also used if you choose to omit a component's render() method. + | + */ + + 'view_path' => resource_path('views/livewire'), + + /* + |--------------------------------------------------------------------------- + | Temporary File Uploads + |--------------------------------------------------------------------------- + | + | Livewire handles file uploads by storing uploads in a temporary directory + | before the file is stored permanently. All file uploads are directed to + | a global endpoint for temporary storage. You may configure this below: + | + */ + + 'temporary_file_upload' => [ + 'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default' + 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) + 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' + 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' + 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... + 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', + 'mov', 'avi', 'wmv', 'mp3', 'm4a', + 'jpg', 'jpeg', 'mpga', 'webp', 'wma', + ], + 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... + 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... + ], + + /* + |--------------------------------------------------------------------------- + | Render On Redirect + |--------------------------------------------------------------------------- + | + | This value determines if Livewire will run a component's `render()` method + | after a redirect has been triggered using something like `redirect(...)` + | Setting this to true will render the view once more before redirecting + | + */ + + 'render_on_redirect' => false, + + /* + |--------------------------------------------------------------------------- + | Eloquent Model Binding + |--------------------------------------------------------------------------- + | + | Previous versions of Livewire supported binding directly to eloquent model + | properties using wire:model by default. However, this behavior has been + | deemed too "magical" and has therefore been put under a feature flag. + | + */ + + 'legacy_model_binding' => false, + + /* + |--------------------------------------------------------------------------- + | Auto-inject Frontend Assets + |--------------------------------------------------------------------------- + | + | By default, Livewire automatically injects its JavaScript and CSS into the + | and of pages containing Livewire components. By disabling + | this behavior, you need to use @livewireStyles and @livewireScripts. + | + */ + + 'inject_assets' => false, + + /* + |--------------------------------------------------------------------------- + | Navigate (SPA mode) + |--------------------------------------------------------------------------- + | + | By adding `wire:navigate` to links in your Livewire application, Livewire + | will prevent the default link handling and instead request those pages + | via AJAX, creating an SPA-like effect. Configure this behavior here. + | + */ + + 'navigate' => [ + 'show_progress_bar' => true, + 'progress_bar_color' => '#2299dd', + ], + + /* + |--------------------------------------------------------------------------- + | HTML Morph Markers + |--------------------------------------------------------------------------- + | + | Livewire intelligently "morphs" existing HTML into the newly rendered HTML + | after each update. To make this process more reliable, Livewire injects + | "markers" into the rendered Blade surrounding @if, @class & @foreach. + | + */ + + 'inject_morph_markers' => true, + + /* + |--------------------------------------------------------------------------- + | Smart Wire Keys + |--------------------------------------------------------------------------- + | + | Livewire uses loops and keys used within loops to generate smart keys that + | are applied to nested components that don't have them. This makes using + | nested components more reliable by ensuring that they all have keys. + | + */ + + 'smart_wire_keys' => true, + + /* + |--------------------------------------------------------------------------- + | Pagination Theme + |--------------------------------------------------------------------------- + | + | When enabling Livewire's pagination feature by using the `WithPagination` + | trait, Livewire will use Tailwind templates to render pagination views + | on the page. If you want Bootstrap CSS, you can specify: "bootstrap" + | + */ + + 'pagination_theme' => 'tailwind', + + /* + |--------------------------------------------------------------------------- + | Release Token + |--------------------------------------------------------------------------- + | + | This token is stored client-side and sent along with each request to check + | a users session to see if a new release has invalidated it. If there is + | a mismatch it will throw an error and prompt for a browser refresh. + | + */ + + 'release_token' => 'a', + + /* + |--------------------------------------------------------------------------- + | CSP Safe + |--------------------------------------------------------------------------- + | + | This config is used to determine if Livewire will use the CSP-safe version + | of Alpine in its bundle. This is useful for applications that are using + | strict Content Security Policy (CSP) to protect against XSS attacks. + | + */ + + 'csp_safe' => false, + + /* + |--------------------------------------------------------------------------- + | Payload Guards + |--------------------------------------------------------------------------- + | + | These settings protect against malicious or oversized payloads that could + | cause denial of service. The default values should feel reasonable for + | most web applications. Each can be set to null to disable the limit. + | + */ + + 'payload' => [ + 'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes + 'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths + 'max_calls' => 50, // Maximum method calls per request + 'max_components' => 20, // Maximum components per batch request + ], +]; diff --git a/src/config/logging.php b/src/config/logging.php new file mode 100644 index 0000000..b09cb25 --- /dev/null +++ b/src/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/src/config/mail.php b/src/config/mail.php new file mode 100644 index 0000000..09e2c79 --- /dev/null +++ b/src/config/mail.php @@ -0,0 +1,171 @@ + env('MAIL_MAILER', 'system'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS'), + 'name' => env('MAIL_FROM_NAME', 'Aziros'), + ], + ], + + 'system' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'from' => [ + 'address' => env('MAIL_SYSTEM_USERNAME', env('MAIL_USERNAME')), + 'name' => 'Aziros', + ], + ], + + 'reminder' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'from' => [ + 'address' => env('MAIL_REMINDER_USERNAME', env('MAIL_USERNAME')), + 'name' => 'Aziros Reminder', + ], + ], + + 'aria' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'from' => [ + 'address' => env('MAIL_ARIA_USERNAME', env('MAIL_USERNAME')), + 'name' => 'Aria', + ], + ], + + 'hello' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'from' => [ + 'address' => env('MAIL_HELLO_USERNAME', env('MAIL_USERNAME')), + 'name' => 'Aziros', + ], + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/src/config/queue.php b/src/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/src/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/src/config/reverb.php b/src/config/reverb.php new file mode 100644 index 0000000..91f3880 --- /dev/null +++ b/src/config/reverb.php @@ -0,0 +1,102 @@ + env('REVERB_SERVER', 'reverb'), + + /* + |-------------------------------------------------------------------------- + | Reverb Servers + |-------------------------------------------------------------------------- + | + | Here you may define details for each of the supported Reverb servers. + | Each server has its own configuration options that are defined in + | the array below. You should ensure all the options are present. + | + */ + + 'servers' => [ + + 'reverb' => [ + 'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), + 'port' => env('REVERB_SERVER_PORT', 8080), + 'path' => env('REVERB_SERVER_PATH', ''), + 'hostname' => env('REVERB_HOST'), + 'options' => [ + 'tls' => [], + ], + 'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000), + 'scaling' => [ + 'enabled' => env('REVERB_SCALING_ENABLED', false), + 'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'), + 'server' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'port' => env('REDIS_PORT', '6379'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'database' => env('REDIS_DB', '0'), + 'timeout' => env('REDIS_TIMEOUT', 60), + ], + ], + 'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15), + 'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Reverb Applications + |-------------------------------------------------------------------------- + | + | Here you may define how Reverb applications are managed. If you choose + | to use the "config" provider, you may define an array of apps which + | your server will support, including their connection credentials. + | + */ + + 'apps' => [ + + 'provider' => 'config', + + 'apps' => [ + [ + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'allowed_origins' => ['*'], + 'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60), + 'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30), + 'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'), + 'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000), + 'accept_client_events_from' => env('REVERB_APP_ACCEPT_CLIENT_EVENTS_FROM', 'members'), + 'rate_limiting' => [ + 'enabled' => env('REVERB_APP_RATE_LIMITING_ENABLED', false), + 'max_attempts' => env('REVERB_APP_RATE_LIMIT_MAX_ATTEMPTS', 60), + 'decay_seconds' => env('REVERB_APP_RATE_LIMIT_DECAY_SECONDS', 60), + 'terminate_on_limit' => env('REVERB_APP_RATE_LIMIT_TERMINATE', false), + ], + ], + ], + + ], + +]; diff --git a/src/config/services.php b/src/config/services.php new file mode 100644 index 0000000..d00a529 --- /dev/null +++ b/src/config/services.php @@ -0,0 +1,64 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + + 'stripe' => [ + 'key' => env('STRIPE_KEY'), + 'secret' => env('STRIPE_SECRET'), + 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), + 'currency' => env('STRIPE_CURRENCY', 'eur'), + ], + + 'openai' => [ + 'key' => env('OPENAI_API_KEY'), + 'model' => env('OPENAI_MODEL', 'gpt-4o-mini'), + ], + + 'google_calendar' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => env('GOOGLE_REDIRECT_URI', config('app.api_url').'/integrations/google/callback'), + 'webhook_url' => env('GOOGLE_CALENDAR_WEBHOOK_URL'), + ], + + 'microsoft' => [ + 'client_id' => env('MICROSOFT_CLIENT_ID'), + 'client_secret' => env('MICROSOFT_CLIENT_SECRET'), + 'redirect' => env('MICROSOFT_REDIRECT_URI', config('app.api_url').'/integrations/outlook/callback'), + 'tenant' => env('MICROSOFT_TENANT', 'common'), + ], + +]; diff --git a/src/config/session.php b/src/config/session.php new file mode 100644 index 0000000..f574482 --- /dev/null +++ b/src/config/session.php @@ -0,0 +1,233 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + + /* + |-------------------------------------------------------------------------- + | Session Serialization + |-------------------------------------------------------------------------- + | + | This value controls the serialization strategy for session data, which + | is JSON by default. Setting this to "php" allows the storage of PHP + | objects in the session but can make an application vulnerable to + | "gadget chain" serialization attacks if the APP_KEY is leaked. + | + | Supported: "json", "php" + | + */ + + 'serialization' => 'json', + +]; diff --git a/src/config/sidebar.php b/src/config/sidebar.php new file mode 100644 index 0000000..103c8f0 --- /dev/null +++ b/src/config/sidebar.php @@ -0,0 +1,109 @@ + [ + [ + 'label' => 'nav.dashboard', + 'route' => 'dashboard.index', + 'icon' => 'home', + ], + [ + 'label' => 'nav.assistant', + 'route' => 'agent.index', + 'icon' => 'sparkles', + ], + [ + 'label' => 'nav.calendar', + 'route' => 'calendar.index', + 'icon' => 'calendar', + ], + [ + 'label' => 'nav.tasks', + 'route' => 'tasks.index', + 'icon' => 'check-circle', + ], + [ + 'label' => 'nav.notes', + 'route' => 'notes.index', + 'icon' => 'document-text', + ], + [ + 'label' => 'nav.contacts', + 'route' => 'contacts.index', + 'icon' => 'users', + ], + [ + 'label' => 'nav.activities', + 'route' => 'activities.index', + 'icon' => 'clock', + ], + ], + + 'admin' => [ + [ + 'label' => 'nav.admin_dashboard', + 'route' => 'admin.dashboard', + 'icon' => 'chart-bar', + ], + [ + 'label' => 'nav.admin_users', + 'route' => 'admin.users.index', + 'icon' => 'user-group', + ], + [ + 'label' => 'nav.admin_affiliates', + 'route' => 'admin.affiliates.index', + 'icon' => 'gift', + ], + [ + 'label' => 'nav.admin_plans', + 'route' => 'admin.plans.index', + 'icon' => 'credit-card', + ], + [ + 'label' => 'nav.admin_translations', + 'route' => 'admin.translations.index', + 'icon' => 'language', + ], + [ + 'label' => 'nav.admin_versions', + 'route' => 'admin.versions.index', + 'icon' => 'arrow-path', + ], + ], + + 'tools' => [ + [ + 'label' => 'nav.integrations', + 'route' => 'integrations.index', + 'icon' => 'link', + ], + [ + 'label' => 'nav.automations', + 'route' => 'automations.index', + 'icon' => 'bolt', + ], + [ + 'label' => 'nav.subscription', + 'route' => 'subscription.index', + 'icon' => 'credit-card', + ], + [ + 'label' => 'nav.invoices', + 'route' => 'invoices.index', + 'icon' => 'document-text', + ], + [ + 'label' => 'nav.affiliate', + 'route' => 'settings.affiliate', + 'icon' => 'gift', + ], + [ + 'label' => 'nav.settings', + 'route' => 'settings.index', + 'icon' => 'cog-6-tooth', + ], + ], + +]; diff --git a/src/config/wire-elements-modal.php b/src/config/wire-elements-modal.php new file mode 100644 index 0000000..2950291 --- /dev/null +++ b/src/config/wire-elements-modal.php @@ -0,0 +1,52 @@ + false, + + /* + |-------------------------------------------------------------------------- + | Include JS + |-------------------------------------------------------------------------- + | + | Livewire UI will inject the required Javascript in your blade template. + | If you want to bundle the required Javascript you can set this to false + | and add `require('vendor/wire-elements/modal/resources/js/modal');` + | to your script bundler like webpack. + | + */ + 'include_js' => true, + + /* + |-------------------------------------------------------------------------- + | Modal Component Defaults + |-------------------------------------------------------------------------- + | + | Configure the default properties for a modal component. + | + | Supported modal_max_width + | 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl' + */ + 'component_defaults' => [ + 'modal_max_width' => '2xl', + + 'close_modal_on_click_away' => true, + + 'close_modal_on_escape' => true, + + 'close_modal_on_escape_is_forceful' => true, + + 'dispatch_close_event' => false, + + 'destroy_on_close' => false, + ], +]; diff --git a/src/database/.gitignore b/src/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/src/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/src/database/factories/UserFactory.php b/src/database/factories/UserFactory.php new file mode 100644 index 0000000..c4ceb07 --- /dev/null +++ b/src/database/factories/UserFactory.php @@ -0,0 +1,45 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/src/database/migrations/0001_01_01_000000_create_users_table.php b/src/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..048213d --- /dev/null +++ b/src/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,73 @@ +uuid('id')->primary(); + + // BASIC + $table->string('name'); + $table->string('email')->unique(); + + $table->timestamp('email_verified_at')->nullable(); + + $table->string('password'); + $table->string('status')->default('active'); + $table->string('role')->default('user'); + + // SETTINGS + $table->boolean('reminders_enabled')->default(true); + $table->boolean('notify_in_app')->default(true); + $table->boolean('notify_email')->default(false); + $table->boolean('notify_push')->default(false); + $table->json('settings')->nullable(); + $table->json('notification_settings')->nullable(); + + $table->string('locale')->default('de'); + $table->string('timezone')->default('UTC'); + $table->json('meta')->nullable(); + + + // SYSTEM + $table->rememberToken(); + $table->timestamps(); + + $table->timestamp('deletion_requested_at')->nullable(); + $table->softDeletes(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/src/database/migrations/0001_01_01_000001_create_cache_table.php b/src/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..06dc7a5 --- /dev/null +++ b/src/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->bigInteger('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->bigInteger('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/src/database/migrations/0001_01_01_000002_create_jobs_table.php b/src/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/src/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/src/database/migrations/2026_04_03_223546_create_reminders_table.php b/src/database/migrations/2026_04_03_223546_create_reminders_table.php new file mode 100644 index 0000000..ff9ced2 --- /dev/null +++ b/src/database/migrations/2026_04_03_223546_create_reminders_table.php @@ -0,0 +1,48 @@ +uuid('id')->primary(); + + // RELATION + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + + // CONTENT + $table->string('title'); // "Zahnarzt" + $table->text('description')->nullable(); + + // TIMING + $table->timestamp('remind_at'); + + // STATUS + $table->boolean('is_sent')->default(false); + + // OPTIONAL CONTEXT + $table->string('type')->nullable(); + // z.B. event, shopping, note + + $table->uuid('reference_id')->nullable(); + // spΓ€ter z.B. event_id + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('reminders'); + } +}; diff --git a/src/database/migrations/2026_04_03_223948_create_activities_table.php b/src/database/migrations/2026_04_03_223948_create_activities_table.php new file mode 100644 index 0000000..84f6974 --- /dev/null +++ b/src/database/migrations/2026_04_03_223948_create_activities_table.php @@ -0,0 +1,48 @@ +uuid('id')->primary(); + + // RELATION + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + + // TYPE + $table->string('type'); + // event_created, note_added, reminder_sent etc. + + // CONTENT + $table->string('title'); // "Termin erstellt" + $table->text('description')->nullable(); + + // OPTIONAL CONTEXT + $table->string('subject_type')->nullable(); + // z.B. reminder, contact, event + + $table->uuid('subject_id')->nullable(); + + // EXTRA (fΓΌr spΓ€tere Erweiterung) + $table->json('meta')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('activities'); + } +}; diff --git a/src/database/migrations/2026_04_03_224035_create_contacts_table.php b/src/database/migrations/2026_04_03_224035_create_contacts_table.php new file mode 100644 index 0000000..80636ca --- /dev/null +++ b/src/database/migrations/2026_04_03_224035_create_contacts_table.php @@ -0,0 +1,45 @@ +uuid('id')->primary(); + + // OWNER + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + + // BASIC + $table->string('name'); + + // OPTIONAL + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + + // META + $table->string('type')->nullable(); + // privat, kunde, arbeit etc. + + $table->text('notes')->nullable(); + $table->date('birthday')->nullable(); + // SYSTEM + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contacts'); + } +}; diff --git a/src/database/migrations/2026_04_03_224117_create_devices_table.php b/src/database/migrations/2026_04_03_224117_create_devices_table.php new file mode 100644 index 0000000..92cd6a1 --- /dev/null +++ b/src/database/migrations/2026_04_03_224117_create_devices_table.php @@ -0,0 +1,47 @@ +uuid('id')->primary(); + + // RELATION + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + + // DEVICE IDENTIFICATION (WICHTIG!) + $table->string('device_id'); // eindeutige ID vom GerΓ€t + + // PUSH TOKEN + $table->string('push_token')->nullable(); // APNs / Firebase + + // INFO + $table->string('platform'); // ios, android + $table->string('device_name')->nullable(); + + // STATUS + $table->boolean('active')->default(true); + + // OPTIONAL (fΓΌr spΓ€ter sehr wertvoll) + $table->timestamp('last_seen')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('devices'); + } +}; diff --git a/src/database/migrations/2026_04_03_231554_create_plans_table.php b/src/database/migrations/2026_04_03_231554_create_plans_table.php new file mode 100644 index 0000000..921c088 --- /dev/null +++ b/src/database/migrations/2026_04_03_231554_create_plans_table.php @@ -0,0 +1,44 @@ +uuid('id')->primary(); + + $table->string('name'); // Free, Pro + $table->integer('sort')->default(0); + $table->string('plan_key'); // Free, Pro + $table->integer('price'); // in cents (z.B. 1500 = 15€) + + $table->integer('yearly_discount_months')->default(0); + $table->boolean('is_featured')->default(false); // "Beliebt" + + $table->integer('credit_limit')->default(0); + $table->integer('device_limit')->default(1); + + $table->boolean('active')->default(true); + $table->boolean('is_internal')->default(false); + + $table->json('ai_config')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('plans'); + } +}; diff --git a/src/database/migrations/2026_04_03_231605_create_subscriptions_table.php b/src/database/migrations/2026_04_03_231605_create_subscriptions_table.php new file mode 100644 index 0000000..95e42db --- /dev/null +++ b/src/database/migrations/2026_04_03_231605_create_subscriptions_table.php @@ -0,0 +1,63 @@ +uuid('id')->primary(); + + // USER (nicht lΓΆschen!) + $table->foreignUuid('user_id')->nullable()->constrained()->nullOnDelete(); + + // PLAN (optional relation) + $table->foreignUuid('plan_id')->nullable(); + + // SNAPSHOT (KRITISCH!) + $table->string('plan_name'); + $table->string('plan_slug'); + + $table->integer('price'); // final price (z. B. 16500) + $table->integer('original_price')->nullable(); // z. B. 18000 + $table->integer('discount_amount')->nullable(); // z. B. 1500 + + $table->string('interval'); // monthly, yearly + + // STATUS + $table->string('status'); // active, canceled, expired + + // ZEIT + $table->timestamp('starts_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + + // PROVIDER + $table->string('provider'); // mollie, stripe + $table->string('provider_customer_id')->nullable(); + $table->string('provider_subscription_id')->nullable(); + + $table->uuid('gifted_by')->nullable(); + $table->timestamp('gifted_at')->nullable(); + $table->string('gift_reason')->nullable(); + + $table->foreign('gifted_by')->references('id')->on('users')->nullOnDelete(); + + $table->string('provider')->nullable()->change(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscriptions'); + } +}; diff --git a/src/database/migrations/2026_04_03_231620_create_payments_table.php b/src/database/migrations/2026_04_03_231620_create_payments_table.php new file mode 100644 index 0000000..4878451 --- /dev/null +++ b/src/database/migrations/2026_04_03_231620_create_payments_table.php @@ -0,0 +1,49 @@ +uuid('id')->primary(); + + $table->foreignUuid('user_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignUuid('subscription_id')->nullable()->constrained()->nullOnDelete(); + + $table->integer('amount'); + $table->string('currency')->default('EUR'); + + $table->string('status'); // paid, failed, pending + + $table->string('provider'); + $table->string('provider_payment_id')->nullable(); + + $table->string('invoice_id')->nullable(); + $table->string('billing_reason')->nullable(); + $table->unsignedInteger('attempt_count')->default(0); + + $table->timestamp('paid_at')->nullable(); + + $table->index('invoice_id'); + + $table->unique(['provider', 'provider_payment_id'], 'payments_provider_payment_unique'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('payments'); + } +}; diff --git a/src/database/migrations/2026_04_04_003244_create_translations_table.php b/src/database/migrations/2026_04_04_003244_create_translations_table.php new file mode 100644 index 0000000..c3592e0 --- /dev/null +++ b/src/database/migrations/2026_04_04_003244_create_translations_table.php @@ -0,0 +1,33 @@ +uuid('id')->primary(); + + $table->string('key'); // auth.email_required + $table->string('locale'); // de, en + $table->text('value'); + + $table->timestamps(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('translations'); + } +}; diff --git a/src/database/migrations/2026_04_04_223341_create_mail_queues_table.php b/src/database/migrations/2026_04_04_223341_create_mail_queues_table.php new file mode 100644 index 0000000..43efd6a --- /dev/null +++ b/src/database/migrations/2026_04_04_223341_create_mail_queues_table.php @@ -0,0 +1,50 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->nullable()->index(); + + $table->string('type')->default('email'); + + $table->string('to'); + $table->string('subject'); + + $table->text('body')->nullable(); + + $table->json('meta')->nullable(); + $table->string('status')->default('pending'); + $table->string('template'); + $table->string('locale')->default('en'); + + $table->integer('tries')->default(0); + $table->integer('max_tries')->default(3); + + $table->timestamp('available_at')->nullable(); + $table->timestamp('sent_at')->nullable(); + $table->text('error')->nullable(); + + $table->timestamps(); + + $table->index(['status', 'available_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('mail_queues'); + } +}; diff --git a/src/database/migrations/2026_04_05_195243_create_verifications_table.php b/src/database/migrations/2026_04_05_195243_create_verifications_table.php new file mode 100644 index 0000000..81a5e8c --- /dev/null +++ b/src/database/migrations/2026_04_05_195243_create_verifications_table.php @@ -0,0 +1,42 @@ +uuid('id')->primary(); + + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + + $table->string('type'); // email, sms, 2fa + $table->string('channel'); // email, sms, app + + $table->string('code')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamp('verified_at')->nullable(); + + $table->integer('resends')->default(0); + $table->timestamp('resends_reset_at')->nullable(); + + $table->timestamps(); + + $table->index(['user_id', 'type']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('verifications'); + } +}; diff --git a/src/database/migrations/2026_04_05_220804_create_agent_logs_table.php b/src/database/migrations/2026_04_05_220804_create_agent_logs_table.php new file mode 100644 index 0000000..c79123f --- /dev/null +++ b/src/database/migrations/2026_04_05_220804_create_agent_logs_table.php @@ -0,0 +1,54 @@ +uuid('id')->primary(); + $table->uuid('user_id'); + + $table->string('type')->nullable(); + // z.B. event, note, task, system + + $table->text('input'); + + $table->json('output')->nullable(); + $table->string('status'); + + $table->integer('credits')->default(0); + + // πŸ”₯ AI Token Tracking (wichtig fΓΌr Kosten + Debugging) + $table->integer('prompt_tokens')->default(0); + $table->integer('completion_tokens')->default(0); + $table->integer('total_tokens')->default(0); + + $table->string('model')->nullable(); + $table->decimal('cost_usd', 10, 6)->default(0); + // πŸ”₯ Performance + $table->integer('duration_ms')->nullable(); + + // πŸ”₯ Raw AI Response (optional fΓΌr Debugging) + $table->json('ai_response')->nullable(); + + $table->timestamps(); + + $table->index('user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('agent_logs'); + } +}; diff --git a/src/database/migrations/2026_04_05_223224_create_events_table.php b/src/database/migrations/2026_04_05_223224_create_events_table.php new file mode 100644 index 0000000..b9e5e7f --- /dev/null +++ b/src/database/migrations/2026_04_05_223224_create_events_table.php @@ -0,0 +1,53 @@ +uuid('id')->primary(); + + $table->uuid('user_id'); + + $table->string('title'); + $table->string('color')->nullable(); + $table->string('google_event_id')->nullable(); + + $table->timestamp('starts_at'); + $table->timestamp('ends_at')->nullable(); + $table->boolean('is_all_day')->default(false); + + $table->date('start_date')->nullable(); + $table->date('end_date')->nullable(); + + $table->text('notes')->nullable(); + $table->json('reminders')->nullable(); + + $table->string('recurrence')->nullable(); + $table->date('recurrence_end_date')->nullable(); + + $table->json('exceptions')->nullable(); + $table->json('participants')->nullable(); + $table->timestamps(); + + $table->index('user_id'); + $table->index('starts_at'); + $table->index('google_event_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('events'); + } +}; diff --git a/src/database/migrations/2026_04_06_212207_create_feature_groups_table.php b/src/database/migrations/2026_04_06_212207_create_feature_groups_table.php new file mode 100644 index 0000000..a3a5363 --- /dev/null +++ b/src/database/migrations/2026_04_06_212207_create_feature_groups_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + + $table->string('key')->unique(); // organisation + $table->string('label'); // Organisation + $table->integer('sort')->default(0); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('feature_groups'); + } +}; diff --git a/src/database/migrations/2026_04_06_212208_create_features_table.php b/src/database/migrations/2026_04_06_212208_create_features_table.php new file mode 100644 index 0000000..b489918 --- /dev/null +++ b/src/database/migrations/2026_04_06_212208_create_features_table.php @@ -0,0 +1,40 @@ +uuid('id')->primary(); + + $table->uuid('feature_group_id'); + + $table->string('key')->unique(); // calendar, notes + $table->string('label'); // Anzeige + + $table->string('group')->nullable(); // optional (core, premium) + $table->string('icon')->nullable(); + + $table->integer('sort')->default(0); + + $table->boolean('active')->default(true); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('features'); + } +}; diff --git a/src/database/migrations/2026_04_06_212250_create_feature_plan_table.php b/src/database/migrations/2026_04_06_212250_create_feature_plan_table.php new file mode 100644 index 0000000..48a6ef2 --- /dev/null +++ b/src/database/migrations/2026_04_06_212250_create_feature_plan_table.php @@ -0,0 +1,36 @@ +uuid('plan_id'); + $table->uuid('feature_id'); + + $table->primary(['plan_id', 'feature_id']); + + $table->index('plan_id'); + $table->index('feature_id'); + + $table->foreign('plan_id')->references('id')->on('plans')->cascadeOnDelete(); + $table->foreign('feature_id')->references('id')->on('features')->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('feature_plan'); + } +}; diff --git a/src/database/migrations/2026_04_07_164352_create_event_duration_stats_table.php b/src/database/migrations/2026_04_07_164352_create_event_duration_stats_table.php new file mode 100644 index 0000000..07f824a --- /dev/null +++ b/src/database/migrations/2026_04_07_164352_create_event_duration_stats_table.php @@ -0,0 +1,36 @@ +uuid('id')->primary(); + + $table->uuid('user_id')->index(); // πŸ”₯ NEU + $table->string('keyword'); + + $table->integer('avg_duration'); + $table->integer('count')->default(1); + + $table->timestamps(); + + $table->unique(['user_id', 'keyword']); // πŸ”₯ wichtig! + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('event_duration_stats'); + } +}; diff --git a/src/database/migrations/2026_04_07_173346_create_event_keywords_table.php b/src/database/migrations/2026_04_07_173346_create_event_keywords_table.php new file mode 100644 index 0000000..50b69f9 --- /dev/null +++ b/src/database/migrations/2026_04_07_173346_create_event_keywords_table.php @@ -0,0 +1,34 @@ +uuid('id')->primary(); + + $table->uuid('user_id')->index(); + $table->string('original'); // kompletter Titel (normalisiert) + $table->string('keyword'); // gelerntes Keyword + + $table->timestamps(); + + $table->unique(['user_id', 'original']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('event_keywords'); + } +}; diff --git a/src/database/migrations/2026_04_08_181424_create_event_user_table.php b/src/database/migrations/2026_04_08_181424_create_event_user_table.php new file mode 100644 index 0000000..984f619 --- /dev/null +++ b/src/database/migrations/2026_04_08_181424_create_event_user_table.php @@ -0,0 +1,30 @@ +uuid('event_id'); + $table->uuid('user_id'); + + $table->primary(['event_id', 'user_id']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('event_user'); + } +}; diff --git a/src/database/migrations/2026_04_11_000001_create_calendar_integrations_table.php b/src/database/migrations/2026_04_11_000001_create_calendar_integrations_table.php new file mode 100644 index 0000000..b3569a8 --- /dev/null +++ b/src/database/migrations/2026_04_11_000001_create_calendar_integrations_table.php @@ -0,0 +1,43 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + + $table->string('provider'); // google | outlook + $table->string('provider_email')->nullable(); + $table->string('calendar_id')->nullable(); + $table->string('calendar_name')->nullable(); + + $table->text('access_token'); + $table->text('refresh_token')->nullable(); + $table->timestamp('token_expires_at')->nullable(); + + // read = nur lesen | write = nur schreiben | both = beidseitig + $table->string('sync_mode')->default('read'); + $table->text('sync_token')->nullable(); + $table->timestamp('last_synced_at')->nullable(); + + $table->string('watch_channel_id')->nullable(); + $table->string('watch_resource_id')->nullable(); + $table->timestamp('watch_expires_at')->nullable(); + + $table->timestamps(); + + $table->unique(['user_id', 'provider']); + }); + } + + public function down(): void + { + Schema::dropIfExists('calendar_integrations'); + } +}; diff --git a/src/database/migrations/2026_04_12_000001_create_automations_table.php b/src/database/migrations/2026_04_12_000001_create_automations_table.php new file mode 100644 index 0000000..a053676 --- /dev/null +++ b/src/database/migrations/2026_04_12_000001_create_automations_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + + $table->string('type'); // event_reminder | daily_agenda | weekly_overview | event_followup | birthday_reminder | no_activity_reminder + $table->string('name'); + + $table->boolean('active')->default(true); + $table->json('config')->nullable(); + + $table->timestamp('last_run_at')->nullable(); + $table->timestamps(); + + $table->unique(['user_id', 'type']); + }); + } + + public function down(): void + { + Schema::dropIfExists('automations'); + } +}; diff --git a/src/database/migrations/2026_04_12_100001_create_notes_table.php b/src/database/migrations/2026_04_12_100001_create_notes_table.php new file mode 100644 index 0000000..5dbb1be --- /dev/null +++ b/src/database/migrations/2026_04_12_100001_create_notes_table.php @@ -0,0 +1,30 @@ +uuid('id')->primary(); + $table->uuid('user_id'); + + $table->string('title')->nullable(); + $table->text('content'); + $table->string('color')->default('yellow'); // yellow|blue|green|pink|purple|gray + $table->boolean('pinned')->default(false); + + $table->timestamps(); + + $table->index(['user_id', 'pinned', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('notes'); + } +}; diff --git a/src/database/migrations/2026_04_12_100002_create_tasks_table.php b/src/database/migrations/2026_04_12_100002_create_tasks_table.php new file mode 100644 index 0000000..df43bcf --- /dev/null +++ b/src/database/migrations/2026_04_12_100002_create_tasks_table.php @@ -0,0 +1,32 @@ +uuid('id')->primary(); + $table->uuid('user_id'); + + $table->string('title'); + $table->text('description')->nullable(); + $table->string('status')->default('pending'); // pending | in_progress | done + $table->string('priority')->default('medium'); // low | medium | high + $table->timestamp('due_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + + $table->timestamps(); + + $table->index(['user_id', 'status', 'due_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('tasks'); + } +}; diff --git a/src/database/migrations/2026_04_15_000002_create_affiliate_tables.php b/src/database/migrations/2026_04_15_000002_create_affiliate_tables.php new file mode 100644 index 0000000..aa48204 --- /dev/null +++ b/src/database/migrations/2026_04_15_000002_create_affiliate_tables.php @@ -0,0 +1,61 @@ +uuid('id')->primary(); + $table->uuid('user_id')->unique(); + $table->string('code', 12)->unique(); + $table->string('status')->default('active'); // active, paused, banned + $table->integer('total_referrals')->default(0); + $table->integer('qualified_referrals')->default(0); + $table->integer('total_credits_earned')->default(0); + $table->integer('pending_credits')->default(0); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + }); + + Schema::create('affiliate_referrals', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->uuid('affiliate_id'); + $table->uuid('referred_user_id'); + $table->string('affiliate_code'); + $table->string('status')->default('pending'); // pending, qualified, paid, cancelled + $table->timestamp('registered_at'); + $table->timestamp('qualifies_at'); + $table->timestamp('paid_at')->nullable(); + $table->integer('credits_awarded')->default(0); + $table->timestamps(); + + $table->foreign('affiliate_id')->references('id')->on('affiliates')->cascadeOnDelete(); + $table->foreign('referred_user_id')->references('id')->on('users')->cascadeOnDelete(); + $table->index('status'); + $table->index('qualifies_at'); + }); + + Schema::create('affiliate_credit_logs', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->uuid('affiliate_id'); + $table->uuid('referral_id'); + $table->integer('credits'); + $table->string('reason'); + $table->timestamps(); + + $table->foreign('affiliate_id')->references('id')->on('affiliates')->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists('affiliate_credit_logs'); + Schema::dropIfExists('affiliate_referrals'); + Schema::dropIfExists('affiliates'); + } +}; diff --git a/src/database/migrations/2026_04_15_100001_create_credit_transactions_table.php b/src/database/migrations/2026_04_15_100001_create_credit_transactions_table.php new file mode 100644 index 0000000..6ffbd74 --- /dev/null +++ b/src/database/migrations/2026_04_15_100001_create_credit_transactions_table.php @@ -0,0 +1,44 @@ +uuid('id')->primary(); + $table->uuid('user_id'); + $table->integer('amount'); + $table->enum('type', [ + 'onboarding', + 'affiliate', + 'admin_gift', + 'usage', + 'refund', + 'subscription', + ]); + $table->string('description'); + $table->uuid('reference_id')->nullable(); + $table->string('reference_type')->nullable(); + $table->uuid('created_by')->nullable(); + $table->timestamps(); + + $table->foreign('user_id') + ->references('id')->on('users') + ->cascadeOnDelete(); + $table->foreign('created_by') + ->references('id')->on('users') + ->nullOnDelete(); + $table->index(['user_id', 'type']); + $table->index(['user_id', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('credit_transactions'); + } +}; diff --git a/src/database/migrations/2026_04_15_200001_create_api_tokens_table.php b/src/database/migrations/2026_04_15_200001_create_api_tokens_table.php new file mode 100644 index 0000000..b351214 --- /dev/null +++ b/src/database/migrations/2026_04_15_200001_create_api_tokens_table.php @@ -0,0 +1,28 @@ +uuid('id')->primary(); + $table->string('user_id'); + $table->string('token', 64)->unique(); + $table->string('name')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + $table->index('token'); + }); + } + + public function down(): void + { + Schema::dropIfExists('api_tokens'); + } +}; diff --git a/src/database/migrations/2026_04_17_100003_create_notifications_table.php b/src/database/migrations/2026_04_17_100003_create_notifications_table.php new file mode 100644 index 0000000..ac07289 --- /dev/null +++ b/src/database/migrations/2026_04_17_100003_create_notifications_table.php @@ -0,0 +1,29 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + $table->string('type'); + $table->string('title'); + $table->string('message')->nullable(); + $table->json('data')->nullable(); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/src/database/migrations/2026_04_17_100004_create_sent_reminders_table.php b/src/database/migrations/2026_04_17_100004_create_sent_reminders_table.php new file mode 100644 index 0000000..f27a0db --- /dev/null +++ b/src/database/migrations/2026_04_17_100004_create_sent_reminders_table.php @@ -0,0 +1,24 @@ +uuid('id')->primary(); + $table->foreignUuid('event_id')->constrained()->cascadeOnDelete(); + $table->string('reminder_hash'); + $table->timestamp('sent_at'); + $table->unique(['event_id', 'reminder_hash']); + }); + } + + public function down(): void + { + Schema::dropIfExists('sent_reminders'); + } +}; diff --git a/src/database/migrations/2026_04_17_100005_create_email_logs_table.php b/src/database/migrations/2026_04_17_100005_create_email_logs_table.php new file mode 100644 index 0000000..fa0d251 --- /dev/null +++ b/src/database/migrations/2026_04_17_100005_create_email_logs_table.php @@ -0,0 +1,25 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained()->cascadeOnDelete(); + $table->string('to'); + $table->string('subject')->nullable(); + $table->string('type')->default('system'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('email_logs'); + } +}; diff --git a/src/database/migrations/2026_04_18_000004_create_event_contact_table.php b/src/database/migrations/2026_04_18_000004_create_event_contact_table.php new file mode 100644 index 0000000..e7f3ca7 --- /dev/null +++ b/src/database/migrations/2026_04_18_000004_create_event_contact_table.php @@ -0,0 +1,22 @@ +foreignUuid('event_id')->constrained()->cascadeOnDelete(); + $table->foreignUuid('contact_id')->constrained()->cascadeOnDelete(); + $table->primary(['event_id', 'contact_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('event_contact'); + } +}; diff --git a/src/database/migrations/2026_04_18_083033_alter_sent_reminders_nullable_event_and_type.php b/src/database/migrations/2026_04_18_083033_alter_sent_reminders_nullable_event_and_type.php new file mode 100644 index 0000000..c5f763a --- /dev/null +++ b/src/database/migrations/2026_04_18_083033_alter_sent_reminders_nullable_event_and_type.php @@ -0,0 +1,35 @@ +dropForeign(['event_id']); + $table->dropUnique(['event_id', 'reminder_hash']); + $table->uuid('event_id')->nullable()->change(); + $table->string('type')->default('event')->after('reminder_hash'); + $table->unique(['reminder_hash', 'type'], 'sent_reminders_hash_type_unique'); + }); + + Schema::table('sent_reminders', function (Blueprint $table) { + $table->foreign('event_id')->references('id')->on('events')->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::table('sent_reminders', function (Blueprint $table) { + $table->dropForeign(['event_id']); + $table->dropUnique('sent_reminders_hash_type_unique'); + $table->dropColumn('type'); + $table->uuid('event_id')->nullable(false)->change(); + $table->unique(['event_id', 'reminder_hash']); + $table->foreign('event_id')->references('id')->on('events')->cascadeOnDelete(); + }); + } +}; diff --git a/src/database/migrations/2026_04_18_104656_add_reminder_at_to_tasks_table.php b/src/database/migrations/2026_04_18_104656_add_reminder_at_to_tasks_table.php new file mode 100644 index 0000000..eb14161 --- /dev/null +++ b/src/database/migrations/2026_04_18_104656_add_reminder_at_to_tasks_table.php @@ -0,0 +1,26 @@ +timestamp('reminder_at')->nullable()->after('due_at'); + $table->boolean('reminder_sent')->default(false)->after('reminder_at'); + }); + } + + public function down(): void + { + Schema::table('tasks', function (Blueprint $table) { + $table->dropColumn(['reminder_at', 'reminder_sent']); + }); + } +}; diff --git a/src/database/migrations/2026_04_18_160645_create_versions_table.php b/src/database/migrations/2026_04_18_160645_create_versions_table.php new file mode 100644 index 0000000..b2853cd --- /dev/null +++ b/src/database/migrations/2026_04_18_160645_create_versions_table.php @@ -0,0 +1,28 @@ +uuid('id')->primary(); + $table->string('version'); + $table->string('name'); + $table->text('changelog')->nullable(); + $table->enum('status', ['draft', 'beta', 'live', 'rollback'])->default('draft'); + $table->enum('platform', ['all', 'web', 'app'])->default('all'); + $table->boolean('show_popup')->default(true); + $table->timestamp('released_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('versions'); + } +}; diff --git a/src/database/seeders/DatabaseSeeder.php b/src/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..6b901f8 --- /dev/null +++ b/src/database/seeders/DatabaseSeeder.php @@ -0,0 +1,25 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/src/database/seeders/ErrorTranslationSeeder.php b/src/database/seeders/ErrorTranslationSeeder.php new file mode 100644 index 0000000..1a8ddd5 --- /dev/null +++ b/src/database/seeders/ErrorTranslationSeeder.php @@ -0,0 +1,71 @@ + 'errors.back_home', 'locale' => 'de', 'value' => 'Zur Startseite'], + ['key' => 'errors.go_back', 'locale' => 'de', 'value' => 'ZurΓΌck'], + + ['key' => 'errors.400.title', 'locale' => 'de', 'value' => 'UngΓΌltige Anfrage'], + ['key' => 'errors.400.message', 'locale' => 'de', 'value' => 'Die Anfrage konnte nicht verarbeitet werden. Bitte ΓΌberprΓΌfe deine Eingaben und versuche es erneut.'], + + ['key' => 'errors.401.title', 'locale' => 'de', 'value' => 'Nicht autorisiert'], + ['key' => 'errors.401.message', 'locale' => 'de', 'value' => 'Du musst angemeldet sein, um auf diese Seite zugreifen zu kΓΆnnen.'], + + ['key' => 'errors.403.title', 'locale' => 'de', 'value' => 'Zugriff verweigert'], + ['key' => 'errors.403.message', 'locale' => 'de', 'value' => 'Du hast keine Berechtigung, auf diese Seite zuzugreifen.'], + + ['key' => 'errors.404.title', 'locale' => 'de', 'value' => 'Seite nicht gefunden'], + ['key' => 'errors.404.message', 'locale' => 'de', 'value' => 'Die angeforderte Seite existiert nicht oder wurde verschoben.'], + + ['key' => 'errors.500.title', 'locale' => 'de', 'value' => 'Interner Serverfehler'], + ['key' => 'errors.500.message', 'locale' => 'de', 'value' => 'Etwas ist schiefgelaufen. Wir arbeiten daran, das Problem zu beheben.'], + + ['key' => 'errors.501.title', 'locale' => 'de', 'value' => 'Nicht implementiert'], + ['key' => 'errors.501.message', 'locale' => 'de', 'value' => 'Diese Funktion ist derzeit nicht verfΓΌgbar.'], + + ['key' => 'errors.502.title', 'locale' => 'de', 'value' => 'Bad Gateway'], + ['key' => 'errors.502.message', 'locale' => 'de', 'value' => 'Der Server hat eine ungΓΌltige Antwort erhalten. Bitte versuche es in wenigen Minuten erneut.'], + + // English + ['key' => 'errors.back_home', 'locale' => 'en', 'value' => 'Back to Home'], + ['key' => 'errors.go_back', 'locale' => 'en', 'value' => 'Go Back'], + + ['key' => 'errors.400.title', 'locale' => 'en', 'value' => 'Bad Request'], + ['key' => 'errors.400.message', 'locale' => 'en', 'value' => 'The request could not be processed. Please check your input and try again.'], + + ['key' => 'errors.401.title', 'locale' => 'en', 'value' => 'Unauthorized'], + ['key' => 'errors.401.message', 'locale' => 'en', 'value' => 'You must be logged in to access this page.'], + + ['key' => 'errors.403.title', 'locale' => 'en', 'value' => 'Forbidden'], + ['key' => 'errors.403.message', 'locale' => 'en', 'value' => 'You do not have permission to access this page.'], + + ['key' => 'errors.404.title', 'locale' => 'en', 'value' => 'Page Not Found'], + ['key' => 'errors.404.message', 'locale' => 'en', 'value' => 'The page you are looking for does not exist or has been moved.'], + + ['key' => 'errors.500.title', 'locale' => 'en', 'value' => 'Internal Server Error'], + ['key' => 'errors.500.message', 'locale' => 'en', 'value' => 'Something went wrong. We are working to fix the issue.'], + + ['key' => 'errors.501.title', 'locale' => 'en', 'value' => 'Not Implemented'], + ['key' => 'errors.501.message', 'locale' => 'en', 'value' => 'This feature is currently not available.'], + + ['key' => 'errors.502.title', 'locale' => 'en', 'value' => 'Bad Gateway'], + ['key' => 'errors.502.message', 'locale' => 'en', 'value' => 'The server received an invalid response. Please try again in a few minutes.'], + ]; + + foreach ($translations as $t) { + Translation::updateOrCreate( + ['key' => $t['key'], 'locale' => $t['locale']], + ['value' => $t['value']] + ); + } + } +} diff --git a/src/database/seeders/FeatureGroupSeeder.php b/src/database/seeders/FeatureGroupSeeder.php new file mode 100644 index 0000000..2a3198a --- /dev/null +++ b/src/database/seeders/FeatureGroupSeeder.php @@ -0,0 +1,70 @@ + 'organisation', + 'label' => 'Organisation', + 'sort' => 1, + ]); + + $productivity = FeatureGroup::create([ + 'key' => 'productivity', + 'label' => 'ProduktivitΓ€t', + 'sort' => 2, + ]); + + $performance = FeatureGroup::create([ + 'key' => 'performance', + 'label' => 'Performance', + 'sort' => 3, + ]); + + + Feature::create([ + 'feature_group_id' => $organisation->id, + 'key' => 'calendar', + 'label' => 'Kalender & Termine', + 'icon' => 'heroicon-o-calendar', + 'sort' => 1, + 'active' => true, + ]); + + Feature::create([ + 'feature_group_id' => $organisation->id, + 'key' => 'reminders', + 'label' => 'Erinnerungen', + 'icon' => 'heroicon-o-bell', + 'sort' => 2, + 'active' => true, + ]); + Feature::create([ + 'feature_group_id' => $productivity->id, + 'key' => 'notes', + 'label' => 'Notizen & Einkaufsliste', + 'icon' => 'heroicon-o-document-text', + 'sort' => 1, + 'active' => true, + ]); + Feature::create([ + 'feature_group_id' => $performance->id, + 'key' => 'speed', + 'label' => 'Schnellere Verarbeitung', + 'icon' => 'heroicon-o-bolt', + 'sort' => 1, + 'active' => true, + ]); + } +} diff --git a/src/database/seeders/FeatureSeeder.php b/src/database/seeders/FeatureSeeder.php new file mode 100644 index 0000000..506b131 --- /dev/null +++ b/src/database/seeders/FeatureSeeder.php @@ -0,0 +1,102 @@ + 'organisation'], + ['label' => 'Organisation', 'sort' => 1] + ); + + $productivity = FeatureGroup::updateOrCreate( + ['key' => 'productivity'], + ['label' => 'ProduktivitΓ€t', 'sort' => 2] + ); + + $integration = FeatureGroup::updateOrCreate( + ['key' => 'integration'], + ['label' => 'Integrationen', 'sort' => 3] + ); + + $performance = FeatureGroup::updateOrCreate( + ['key' => 'performance'], + ['label' => 'Performance', 'sort' => 4] + ); + + // ── Features (upsert) ──────────────────────────────────────── + + $features = [ + ['key' => 'calendar', 'label' => 'Kalender & Termine', 'icon' => 'heroicon-o-calendar', 'group' => $organisation, 'sort' => 1], + ['key' => 'reminders', 'label' => 'Erinnerungen', 'icon' => 'heroicon-o-bell', 'group' => $organisation, 'sort' => 2], + ['key' => 'tasks', 'label' => 'Aufgaben', 'icon' => 'heroicon-o-check-circle', 'group' => $organisation, 'sort' => 3], + ['key' => 'notes', 'label' => 'Notizen', 'icon' => 'heroicon-o-document-text', 'group' => $productivity, 'sort' => 1], + ['key' => 'contacts', 'label' => 'Kontakte', 'icon' => 'heroicon-o-users', 'group' => $productivity, 'sort' => 2], + ['key' => 'ai_agent', 'label' => 'KI-Assistent', 'icon' => 'heroicon-o-sparkles', 'group' => $productivity, 'sort' => 3], + ['key' => 'calendar_sync', 'label' => 'Kalender-Synchronisierung', 'icon' => 'heroicon-o-arrow-path', 'group' => $integration, 'sort' => 1], + ['key' => 'automations', 'label' => 'Automationen', 'icon' => 'heroicon-o-bolt', 'group' => $integration, 'sort' => 2], + ['key' => 'speed', 'label' => 'GPT-4o Modell (schneller & prΓ€ziser)', 'icon' => 'heroicon-o-bolt', 'group' => $performance, 'sort' => 1], + ]; + + $featureIds = []; + foreach ($features as $f) { + $feature = Feature::updateOrCreate( + ['key' => $f['key']], + [ + 'label' => $f['label'], + 'icon' => $f['icon'], + 'feature_group_id' => $f['group']->id, + 'sort' => $f['sort'], + 'active' => true, + ] + ); + $featureIds[$f['key']] = $feature->id; + } + + // ── Plan β†’ Feature Zuordnung ───────────────────────────────── + + // Features die jeder Plan bekommt (Free + Pro) + $freeFeatures = ['calendar', 'reminders', 'tasks', 'notes', 'contacts', 'ai_agent']; + + // Features die nur Pro bekommt + $proFeatures = ['calendar_sync', 'automations', 'speed']; + + // Bestehende VerknΓΌpfungen leeren + DB::table('feature_plan')->delete(); + + // Alle PlΓ€ne laden + $plans = Plan::all()->keyBy('plan_key'); + + foreach ($plans as $planKey => $plan) { + $assignFeatures = $freeFeatures; + + // Pro-PlΓ€ne und interne PlΓ€ne bekommen alle Features + if ($plan->price > 0 || $plan->is_internal) { + $assignFeatures = array_merge($assignFeatures, $proFeatures); + } + + foreach ($assignFeatures as $featureKey) { + if (!isset($featureIds[$featureKey])) continue; + + DB::table('feature_plan')->insert([ + 'plan_id' => $plan->id, + 'feature_id' => $featureIds[$featureKey], + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + $this->command->info("{$plan->name}: " . count($assignFeatures) . " Features zugewiesen"); + } + } +} diff --git a/src/database/seeders/InternalPlanSeeder.php b/src/database/seeders/InternalPlanSeeder.php new file mode 100644 index 0000000..2551fd0 --- /dev/null +++ b/src/database/seeders/InternalPlanSeeder.php @@ -0,0 +1,37 @@ +exists()) { + $this->command->info('Internal Plan existiert bereits.'); + return; + } + + Plan::create([ + 'name' => 'Internal', + 'sort' => 999, + 'plan_key' => 'internal', + 'price' => 0, + 'credit_limit' => 0, + 'device_limit' => 99, + 'active' => true, + 'is_internal' => true, + 'ai_config' => [ + 'model' => 'gpt-4o', + 'temperature' => 0.5, + 'max_tokens' => 2000, + 'input_cost' => 0.0025, + 'output_cost' => 0.01, + ], + ]); + + $this->command->info('Internal Plan erstellt.'); + } +} diff --git a/src/database/seeders/MigrateBonusCreditsSeeder.php b/src/database/seeders/MigrateBonusCreditsSeeder.php new file mode 100644 index 0000000..b59ccdc --- /dev/null +++ b/src/database/seeders/MigrateBonusCreditsSeeder.php @@ -0,0 +1,51 @@ +where('bonus_credits', '>', 0) + ->get(['id', 'bonus_credits']); + + foreach ($users as $user) { + CreditTransaction::create([ + 'user_id' => $user->id, + 'amount' => $user->bonus_credits, + 'type' => 'onboarding', + 'description' => 'Migriert von bonus_credits', + ]); + } + + $this->command->info("{$users->count()} User-Credits migriert."); + return; + } + + // Spalte bereits entfernt β†’ prΓΌfe ob Transaktionen fehlen + $usersWithoutTx = DB::table('users') + ->leftJoin('credit_transactions', 'users.id', '=', 'credit_transactions.user_id') + ->whereNull('credit_transactions.id') + ->whereNotNull('users.email_verified_at') + ->pluck('users.id'); + + foreach ($usersWithoutTx as $userId) { + CreditTransaction::create([ + 'user_id' => $userId, + 'amount' => 300, + 'type' => 'onboarding', + 'description' => 'Willkommens-Bonus (nachtrΓ€glich migriert)', + ]); + } + + $this->command->info("{$usersWithoutTx->count()} verifizierte User ohne Transaktionen nachtrΓ€glich migriert (300 Credits)."); + } +} diff --git a/src/database/seeders/PaymentSeeder.php b/src/database/seeders/PaymentSeeder.php new file mode 100644 index 0000000..8aadab0 --- /dev/null +++ b/src/database/seeders/PaymentSeeder.php @@ -0,0 +1,98 @@ +first(); + + if (!$user) { + $this->command->warn('Test-User nicht gefunden. Bitte zuerst DatabaseSeeder ausfΓΌhren.'); + return; + } + + // Bestehende Daten entfernen + Payment::where('user_id', $user->id)->delete(); + Subscription::where('user_id', $user->id)->delete(); + + // Pro-Plan ermitteln (hΓΆchster sort-Wert unter aktiven PlΓ€nen) + $plan = Plan::where('active', true)->orderByDesc('sort')->first() + ?? Plan::where('active', true)->first(); + + if (!$plan) { + $this->command->warn('Kein aktiver Plan gefunden. Bitte zuerst PlanSeeder ausfΓΌhren.'); + return; + } + + // Subscription erstellen (monatlich, seit 6 Monaten aktiv) + $startsAt = now()->subMonths(6)->startOfMonth(); + $endsAt = now()->addMonths(1)->startOfMonth(); + + $subscription = Subscription::create([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'plan_slug' => $plan->plan_key ?? Str::slug($plan->name), + 'price' => $plan->price, + 'original_price' => $plan->price, + 'discount_amount' => null, + 'interval' => 'monthly', + 'status' => 'active', + 'starts_at' => $startsAt, + 'ends_at' => $endsAt, + 'provider' => 'stripe', + 'provider_customer_id' => 'cus_demo' . Str::random(8), + 'provider_subscription_id' => 'sub_demo' . Str::random(8), + ]); + + // Zahlungen: 5 erfolgreiche monatliche + 1 fehlgeschlagene + 1 ausstehende + $payments = [ + // Fehlgeschlagene Zahlung (vor 4 Monaten, dann erneut versucht) + [ + 'months_ago' => 4, + 'status' => 'failed', + 'paid_at' => null, + ], + // Erfolgreiche Zahlungen (monatlich) + ['months_ago' => 5, 'status' => 'paid'], + ['months_ago' => 4, 'status' => 'paid'], // Wiederholung nach Failed + ['months_ago' => 3, 'status' => 'paid'], + ['months_ago' => 2, 'status' => 'paid'], + ['months_ago' => 1, 'status' => 'paid'], + // Aktuelle Periode: ausstehend + [ + 'months_ago' => 0, + 'status' => 'pending', + 'paid_at' => null, + ], + ]; + + foreach ($payments as $p) { + $date = now()->subMonths($p['months_ago'])->startOfMonth(); + + Payment::create([ + 'user_id' => $user->id, + 'subscription_id' => $subscription->id, + 'amount' => $plan->price, + 'currency' => 'EUR', + 'status' => $p['status'], + 'provider' => 'stripe', + 'provider_payment_id' => 'pi_demo' . Str::random(12), + 'paid_at' => $p['status'] === 'paid' ? $date : ($p['paid_at'] ?? null), + 'created_at' => $date, + 'updated_at' => $date, + ]); + } + + $this->command->info("Subscription + {$subscription->plan_name}-Plan + " . count($payments) . " Zahlungen fΓΌr {$user->email} erstellt."); + } +} diff --git a/src/database/seeders/PlanSeeder.php b/src/database/seeders/PlanSeeder.php new file mode 100644 index 0000000..2380454 --- /dev/null +++ b/src/database/seeders/PlanSeeder.php @@ -0,0 +1,37 @@ + 'Free', + 'slug' => 'free', + 'price' => 0, + 'interval' => 'monthly', + ]); + + Plan::create([ + 'name' => 'Pro Monthly', + 'slug' => 'pro_monthly', + 'price' => 1300, + 'interval' => 'monthly', + ]); + + Plan::create([ + 'name' => 'Pro Yearly', + 'slug' => 'pro_yearly', + 'price' => 13000, + 'interval' => 'yearly', + ]); + } +} diff --git a/src/database/seeders/RolePlansSeeder.php b/src/database/seeders/RolePlansSeeder.php new file mode 100644 index 0000000..f51fa06 --- /dev/null +++ b/src/database/seeders/RolePlansSeeder.php @@ -0,0 +1,92 @@ + 'Beta Tester', + 'sort' => 990, + 'plan_key' => 'beta_tester', + 'price' => 0, + 'credit_limit' => 2000, + 'device_limit' => 3, + 'active' => true, + 'is_internal' => true, + 'ai_config' => [ + 'model' => 'gpt-4o-mini', + 'temperature' => 0.5, + 'max_tokens' => 1500, + 'input_cost' => 0.00015, + 'output_cost' => 0.0006, + ], + ], + [ + 'name' => 'Affiliate', + 'sort' => 991, + 'plan_key' => 'affiliate', + 'price' => 0, + 'credit_limit' => 1000, + 'device_limit' => 2, + 'active' => true, + 'is_internal' => true, + 'ai_config' => [ + 'model' => 'gpt-4o-mini', + 'temperature' => 0.5, + 'max_tokens' => 1500, + 'input_cost' => 0.00015, + 'output_cost' => 0.0006, + ], + ], + [ + 'name' => 'Support', + 'sort' => 992, + 'plan_key' => 'support', + 'price' => 0, + 'credit_limit' => 5000, + 'device_limit' => 5, + 'active' => true, + 'is_internal' => true, + 'ai_config' => [ + 'model' => 'gpt-4o', + 'temperature' => 0.5, + 'max_tokens' => 2000, + 'input_cost' => 0.0025, + 'output_cost' => 0.01, + ], + ], + [ + 'name' => 'Developer', + 'sort' => 993, + 'plan_key' => 'developer', + 'price' => 0, + 'credit_limit' => 0, + 'device_limit' => 99, + 'active' => true, + 'is_internal' => true, + 'ai_config' => [ + 'model' => 'gpt-4o', + 'temperature' => 0.5, + 'max_tokens' => 2000, + 'input_cost' => 0.0025, + 'output_cost' => 0.01, + ], + ], + ]; + + foreach ($plans as $plan) { + Plan::updateOrCreate( + ['plan_key' => $plan['plan_key']], + $plan + ); + } + + $this->command->info(count($plans) . ' Rollen-PlΓ€ne erstellt/aktualisiert.'); + } +} diff --git a/src/database/seeders/TranslationSeeder.php b/src/database/seeders/TranslationSeeder.php new file mode 100644 index 0000000..46a14c4 --- /dev/null +++ b/src/database/seeders/TranslationSeeder.php @@ -0,0 +1,1004 @@ + ['de' => 'Konfiguration', 'en' => 'Configuration'], + 'nav.full' => ['de' => 'Voll', 'en' => 'Full'], + 'nav.unlimited' => ['de' => 'Unlimited', 'en' => 'Unlimited'], + 'nav.manage_plan' => ['de' => 'Plan verwalten β†’', 'en' => 'Manage plan β†’'], + 'nav.settings' => ['de' => 'Einstellungen', 'en' => 'Settings'], + 'nav.logout' => ['de' => 'Abmelden', 'en' => 'Log out'], + + // Sidebar – Main + 'nav.dashboard' => ['de' => 'Dashboard', 'en' => 'Dashboard'], + 'nav.assistant' => ['de' => 'Assistent', 'en' => 'Assistant'], + 'nav.calendar' => ['de' => 'Kalender', 'en' => 'Calendar'], + 'nav.tasks' => ['de' => 'Aufgaben', 'en' => 'Tasks'], + 'nav.notes' => ['de' => 'Notizen', 'en' => 'Notes'], + 'nav.contacts' => ['de' => 'Kontakte', 'en' => 'Contacts'], + 'nav.activities' => ['de' => 'AktivitΓ€ten', 'en' => 'Activities'], + + // Sidebar – Tools + 'nav.integrations' => ['de' => 'Integrationen', 'en' => 'Integrations'], + 'nav.automations' => ['de' => 'Automationen', 'en' => 'Automations'], + 'nav.subscription' => ['de' => 'Abonnement', 'en' => 'Subscription'], + 'nav.invoices' => ['de' => 'Rechnungen', 'en' => 'Invoices'], + 'nav.affiliate' => ['de' => 'Affiliate', 'en' => 'Affiliate'], + + // Sidebar – Admin + 'nav.admin_dashboard' => ['de' => 'Dashboard', 'en' => 'Dashboard'], + 'nav.admin_users' => ['de' => 'Benutzer', 'en' => 'Users'], + 'nav.admin_affiliates' => ['de' => 'Affiliates', 'en' => 'Affiliates'], + 'nav.admin_plans' => ['de' => 'PlΓ€ne', 'en' => 'Plans'], + 'nav.admin_translations' => ['de' => 'Übersetzungen', 'en' => 'Translations'], + + // ── Header ────────────────────────────── + 'header.agent_history' => ['de' => 'Agent-Verlauf', 'en' => 'Agent History'], + 'header.subscription' => ['de' => 'Abonnement', 'en' => 'Subscription'], + 'header.user_details' => ['de' => 'Benutzer-Details', 'en' => 'User Details'], + 'header.user_calendar' => ['de' => 'Benutzer-Kalender', 'en' => 'User Calendar'], + 'header.open_menu' => ['de' => 'MenΓΌ ΓΆffnen', 'en' => 'Open menu'], + 'header.toggle_sidebar' => ['de' => 'Sidebar ein-/ausblenden', 'en' => 'Toggle sidebar'], + 'header.notifications' => ['de' => 'Benachrichtigungen', 'en' => 'Notifications'], + 'header.new' => ['de' => 'neu', 'en' => 'new'], + 'header.no_activities' => ['de' => 'Keine AktivitΓ€ten', 'en' => 'No activities'], + 'header.view_all' => ['de' => 'Alle ansehen β†’', 'en' => 'View all β†’'], + 'header.plan_billing' => ['de' => 'Plan & Abrechnung', 'en' => 'Plan & Billing'], + + // ── Common ────────────────────────────── + 'common.save' => ['de' => 'Speichern', 'en' => 'Save'], + 'common.cancel' => ['de' => 'Abbrechen', 'en' => 'Cancel'], + 'common.delete' => ['de' => 'LΓΆschen', 'en' => 'Delete'], + 'common.edit' => ['de' => 'Bearbeiten', 'en' => 'Edit'], + 'common.create' => ['de' => 'Erstellen', 'en' => 'Create'], + 'common.back' => ['de' => 'ZurΓΌck', 'en' => 'Back'], + 'common.close' => ['de' => 'Schließen', 'en' => 'Close'], + 'common.confirm' => ['de' => 'BestΓ€tigen', 'en' => 'Confirm'], + 'common.search' => ['de' => 'Suchen', 'en' => 'Search'], + 'common.filter' => ['de' => 'Filtern', 'en' => 'Filter'], + 'common.all' => ['de' => 'Alle', 'en' => 'All'], + 'common.active' => ['de' => 'Aktiv', 'en' => 'Active'], + 'common.inactive' => ['de' => 'Inaktiv', 'en' => 'Inactive'], + 'common.yes' => ['de' => 'Ja', 'en' => 'Yes'], + 'common.no' => ['de' => 'Nein', 'en' => 'No'], + 'common.loading' => ['de' => 'Laden...', 'en' => 'Loading...'], + 'common.success' => ['de' => 'Erfolgreich', 'en' => 'Success'], + 'common.error' => ['de' => 'Fehler', 'en' => 'Error'], + 'common.warning' => ['de' => 'Warnung', 'en' => 'Warning'], + 'common.today' => ['de' => 'Heute', 'en' => 'Today'], + 'common.tomorrow' => ['de' => 'Morgen', 'en' => 'Tomorrow'], + 'common.week' => ['de' => 'Woche', 'en' => 'Week'], + 'common.month' => ['de' => 'Monat', 'en' => 'Month'], + 'common.year' => ['de' => 'Jahr', 'en' => 'Year'], + 'common.day' => ['de' => 'Tag', 'en' => 'Day'], + 'common.upgrade' => ['de' => 'Upgraden', 'en' => 'Upgrade'], + 'common.setup' => ['de' => 'Einrichten', 'en' => 'Set up'], + 'common.connect' => ['de' => 'Verbinden', 'en' => 'Connect'], + 'common.disconnect' => ['de' => 'Trennen', 'en' => 'Disconnect'], + 'common.send' => ['de' => 'Senden', 'en' => 'Send'], + 'common.reset_filter' => ['de' => 'Filter zurΓΌcksetzen', 'en' => 'Reset filter'], + 'common.of' => ['de' => 'von', 'en' => 'of'], + 'common.entries' => ['de' => 'EintrΓ€gen', 'en' => 'entries'], + 'common.total' => ['de' => 'Gesamt', 'en' => 'Total'], + 'common.name' => ['de' => 'Name', 'en' => 'Name'], + 'common.email' => ['de' => 'E-Mail', 'en' => 'Email'], + 'common.status' => ['de' => 'Status', 'en' => 'Status'], + 'common.actions' => ['de' => 'Aktionen', 'en' => 'Actions'], + 'common.details' => ['de' => 'Details', 'en' => 'Details'], + 'common.until' => ['de' => 'bis', 'en' => 'until'], + 'common.since' => ['de' => 'seit', 'en' => 'since'], + 'common.credits' => ['de' => 'Credits', 'en' => 'Credits'], + 'common.done' => ['de' => 'Erledigt', 'en' => 'Done'], + 'common.from' => ['de' => 'Von', 'en' => 'From'], + 'common.to' => ['de' => 'Bis', 'en' => 'To'], + 'common.saving' => ['de' => 'Speichert…', 'en' => 'Saving…'], + + // ── Auth ──────────────────────────────── + 'auth.enter_code' => ['de' => 'Code eingeben', 'en' => 'Enter code'], + 'auth.verify' => ['de' => 'Verifizieren', 'en' => 'Verify'], + 'auth.resend_code' => ['de' => 'Code erneut senden', 'en' => 'Resend code'], + + // ── Dashboard ─────────────────────────── + 'dashboard.overdue' => ['de' => 'ΓΌberfΓ€llig', 'en' => 'overdue'], + 'dashboard.open_calendar' => ['de' => 'Kalender ΓΆffnen', 'en' => 'Open calendar'], + 'dashboard.events_today' => ['de' => 'Termine heute', 'en' => 'Events today'], + 'dashboard.open_tasks' => ['de' => 'Offene Aufgaben', 'en' => 'Open tasks'], + 'dashboard.notes' => ['de' => 'Notizen', 'en' => 'Notes'], + 'dashboard.unlimited' => ['de' => 'Unlimited', 'en' => 'Unlimited'], + 'dashboard.credits_month' => ['de' => 'Credits (Monat)', 'en' => 'Credits (Month)'], + 'dashboard.all' => ['de' => 'Alle', 'en' => 'All'], + 'dashboard.multiday' => ['de' => 'MehrtΓ€gig', 'en' => 'Multi-day'], + 'dashboard.all_day' => ['de' => 'Ganztag', 'en' => 'All day'], + 'dashboard.no_events' => ['de' => 'Keine Termine heute', 'en' => 'No events today'], + 'dashboard.create_event' => ['de' => 'Termin erstellen', 'en' => 'Create event'], + 'dashboard.next_events' => ['de' => 'NΓ€chste Termine', 'en' => 'Upcoming events'], + 'dashboard.all_tasks_done' => ['de' => 'Alle Aufgaben erledigt', 'en' => 'All tasks completed'], + 'dashboard.new_task' => ['de' => 'Neue Aufgabe', 'en' => 'New task'], + 'dashboard.credits' => ['de' => 'Credits', 'en' => 'Credits'], + 'dashboard.credits_this_month' => ['de' => 'Credits diesen Monat', 'en' => 'Credits this month'], + 'dashboard.no_credit_limit' => ['de' => 'Kein Kredit-Limit konfiguriert', 'en' => 'No credit limit configured'], + 'dashboard.manage_plan' => ['de' => 'Plan verwalten', 'en' => 'Manage plan'], + 'dashboard.activities' => ['de' => 'AktivitΓ€ten', 'en' => 'Activities'], + 'dashboard.no_activities' => ['de' => 'Noch keine AktivitΓ€ten', 'en' => 'No activities yet'], + 'dashboard.ai_assistant' => ['de' => 'KI-Assistent', 'en' => 'AI Assistant'], + 'dashboard.ai_subtitle' => ['de' => 'Erstelle Termine, Aufgaben und Notizen per Spracheingabe', 'en' => 'Create events, tasks and notes by voice'], + 'dashboard.open_assistant' => ['de' => 'Assistent ΓΆffnen', 'en' => 'Open assistant'], + + // ── Agent / Aria ───────────────────────── + 'agent.conversation_ended' => ['de' => 'GesprΓ€ch beendet', 'en' => 'Conversation ended'], + 'agent.ready_new' => ['de' => 'Aria ist bereit fΓΌr ein neues GesprΓ€ch.', 'en' => 'Aria is ready for a new conversation.'], + 'agent.title' => ['de' => 'Aria', 'en' => 'Aria'], + 'agent.subtitle' => ['de' => 'Deine persΓΆnliche Assistentin', 'en' => 'Your personal assistant'], + 'agent.active' => ['de' => 'Aktiv', 'en' => 'Active'], + 'agent.new_conversation' => ['de' => 'Neues GesprΓ€ch', 'en' => 'New conversation'], + 'agent.history' => ['de' => 'Verlauf', 'en' => 'History'], + 'agent.credits' => ['de' => 'Credits', 'en' => 'Credits'], + 'agent.unlimited' => ['de' => 'Unlimited', 'en' => 'Unlimited'], + 'agent.full' => ['de' => 'Voll', 'en' => 'Full'], + 'agent.credits_month' => ['de' => 'Credits diesen Monat', 'en' => 'Credits this month'], + 'agent.plan' => ['de' => 'Plan', 'en' => 'Plan'], + 'agent.balance' => ['de' => 'Guthaben', 'en' => 'Balance'], + 'agent.almost_used' => ['de' => 'Fast aufgebraucht', 'en' => 'Almost used up'], + 'agent.upgrade_pro' => ['de' => 'Upgrade auf Pro', 'en' => 'Upgrade to Pro'], + 'agent.quota_exhausted' => ['de' => 'Kontingent erschΓΆpft', 'en' => 'Quota exhausted'], + 'agent.upgrade_now' => ['de' => 'Jetzt upgraden', 'en' => 'Upgrade now'], + 'agent.ready' => ['de' => 'Bereit', 'en' => 'Ready'], + 'agent.speak_now' => ['de' => 'Sprich jetzt...', 'en' => 'Speak now...'], + 'agent.thinking' => ['de' => 'Denke nach...', 'en' => 'Thinking...'], + 'agent.answering' => ['de' => 'Antwort...', 'en' => 'Responding...'], + 'agent.end_conversation' => ['de' => 'GesprΓ€ch beenden', 'en' => 'End conversation'], + 'agent.greeting' => ['de' => 'Hey! Ich bin Aria.', 'en' => 'Hey! I\'m Aria.'], + 'agent.intro' => ['de' => 'Ich kann Termine verwalten, Notizen und Aufgaben erstellen, Kontakte anlegen, E-Mails verschicken β€” oder wir quatschen einfach.', 'en' => 'I can manage events, create notes and tasks, add contacts, send emails β€” or we can just chat.'], + 'agent.prompt_event' => ['de' => 'Ich brauche einen Termin ', 'en' => 'I need an appointment '], + 'agent.create_event' => ['de' => 'Termin erstellen', 'en' => 'Create event'], + 'agent.prompt_note' => ['de' => 'Mach mir eine Notiz ', 'en' => 'Make me a note '], + 'agent.create_note' => ['de' => 'Notiz anlegen', 'en' => 'Create note'], + 'agent.prompt_chat' => ['de' => 'Hey, wie gehts?', 'en' => 'Hey, how are you?'], + 'agent.just_chat' => ['de' => 'Einfach reden', 'en' => 'Just chat'], + 'agent.placeholder' => ['de' => 'Schreib etwas…', 'en' => 'Type something…'], + 'agent.last_requests' => ['de' => 'Letzte Anfragen', 'en' => 'Recent requests'], + 'agent.view_all' => ['de' => 'Alle ansehen', 'en' => 'View all'], + 'agent.no_requests' => ['de' => 'Noch keine Anfragen', 'en' => 'No requests yet'], + + // ── Agent Type Badges ──────────────────── + 'agent.type.event' => ['de' => 'Termin', 'en' => 'Event'], + 'agent.type.event_update' => ['de' => 'Termin', 'en' => 'Event'], + 'agent.type.note' => ['de' => 'Notiz', 'en' => 'Note'], + 'agent.type.note_update' => ['de' => 'Notiz', 'en' => 'Note'], + 'agent.type.task' => ['de' => 'Aufgabe', 'en' => 'Task'], + 'agent.type.task_update' => ['de' => 'Aufgabe', 'en' => 'Task'], + 'agent.type.contact' => ['de' => 'Kontakt', 'en' => 'Contact'], + 'agent.type.email' => ['de' => 'E-Mail', 'en' => 'Email'], + 'agent.type.chat' => ['de' => 'Chat', 'en' => 'Chat'], + 'agent.type.multi' => ['de' => 'Multi', 'en' => 'Multi'], + 'agent.type.bonus' => ['de' => 'Bonus', 'en' => 'Bonus'], + 'agent.type.system' => ['de' => 'System', 'en' => 'System'], + + // ── Agent Status Labels ────────────────── + 'agent.status.all' => ['de' => 'Alle', 'en' => 'All'], + 'agent.status.success' => ['de' => 'Erfolgreich', 'en' => 'Successful'], + 'agent.status.failed' => ['de' => 'Fehlgeschlagen', 'en' => 'Failed'], + 'agent.status.conflict' => ['de' => 'Konflikt', 'en' => 'Conflict'], + 'agent.status.duplicate' => ['de' => 'Duplikat', 'en' => 'Duplicate'], + 'agent.status.processing' => ['de' => 'Verarbeitung', 'en' => 'Processing'], + 'agent.status.error' => ['de' => 'Fehler', 'en' => 'Error'], + + // ── Agent Logs ────────────────────────── + 'agent.logs.title' => ['de' => 'Agent-Verlauf', 'en' => 'Agent History'], + 'agent.logs.subtitle_admin' => ['de' => 'Alle KI-Anfragen mit Credits, Kosten und Performance', 'en' => 'All AI requests with credits, costs and performance'], + 'agent.logs.subtitle' => ['de' => 'Alle KI-Anfragen und verbrauchte Credits', 'en' => 'All AI requests and credits used'], + 'agent.logs.go_assistant' => ['de' => 'Zum Assistenten', 'en' => 'Go to assistant'], + 'agent.logs.credits_month' => ['de' => 'Credits diesen Monat', 'en' => 'Credits this month'], + 'agent.logs.used' => ['de' => 'verbraucht', 'en' => 'used'], + 'agent.logs.requests_month' => ['de' => 'Anfragen (Monat)', 'en' => 'Requests (Month)'], + 'agent.logs.success_rate' => ['de' => 'Erfolgsrate', 'en' => 'Success rate'], + 'agent.logs.errors' => ['de' => 'Fehler', 'en' => 'Errors'], + 'agent.logs.avg_response' => ['de' => 'Ø Antwortzeit', 'en' => 'Avg. response time'], + 'agent.logs.search' => ['de' => 'Anfrage durchsuchen…', 'en' => 'Search requests…'], + 'agent.logs.no_entries' => ['de' => 'Keine EintrΓ€ge gefunden', 'en' => 'No entries found'], + 'agent.logs.no_entries_hint' => ['de' => 'Anfragen an den Assistenten erscheinen hier', 'en' => 'Requests to the assistant will appear here'], + + // ── Agent Conflict Modal ───────────────── + 'agent.conflict.title' => ['de' => 'Termin-Konflikt', 'en' => 'Event Conflict'], + 'agent.conflict.subtitle' => ['de' => 'Dieser Zeitraum ist bereits belegt', 'en' => 'This time slot is already taken'], + 'agent.conflict.your_event' => ['de' => 'Dein Termin', 'en' => 'Your event'], + 'agent.conflict.earlier' => ['de' => 'FrΓΌher mΓΆglich', 'en' => 'Earlier available'], + 'agent.conflict.later' => ['de' => 'SpΓ€ter mΓΆglich', 'en' => 'Later available'], + 'agent.conflict.hint' => ['de' => 'WΓ€hle eine Zeit oder speichere den Termin trotzdem.', 'en' => 'Choose a time or save the event anyway.'], + 'agent.conflict.use_earlier' => ['de' => 'FrΓΌher ΓΌbernehmen', 'en' => 'Use earlier slot'], + 'agent.conflict.use_later' => ['de' => 'SpΓ€ter ΓΌbernehmen', 'en' => 'Use later slot'], + 'agent.conflict.save_anyway' => ['de' => 'Trotzdem speichern', 'en' => 'Save anyway'], + + // ── Calendar ──────────────────────────── + 'calendar.today' => ['de' => 'Heute', 'en' => 'Today'], + 'calendar.month' => ['de' => 'Monat', 'en' => 'Month'], + 'calendar.week' => ['de' => 'Woche', 'en' => 'Week'], + 'calendar.day' => ['de' => 'Tag', 'en' => 'Day'], + 'calendar.all_day' => ['de' => 'ganzt.', 'en' => 'all day'], + 'calendar.all_day_full' => ['de' => 'GanztΓ€gig', 'en' => 'All day'], + 'calendar.more' => ['de' => 'mehr', 'en' => 'more'], + 'calendar.days.mo' => ['de' => 'Mo', 'en' => 'Mon'], + 'calendar.days.di' => ['de' => 'Di', 'en' => 'Tue'], + 'calendar.days.mi' => ['de' => 'Mi', 'en' => 'Wed'], + 'calendar.days.do' => ['de' => 'Do', 'en' => 'Thu'], + 'calendar.days.fr' => ['de' => 'Fr', 'en' => 'Fri'], + 'calendar.days.sa' => ['de' => 'Sa', 'en' => 'Sat'], + 'calendar.days.so' => ['de' => 'So', 'en' => 'Sun'], + + // ── Calendar Sidebar ───────────────────── + 'calendar.edit_event' => ['de' => 'Termin bearbeiten', 'en' => 'Edit event'], + 'calendar.new_event' => ['de' => 'Neuer Termin', 'en' => 'New event'], + + // ── Calendar Modals ────────────────────── + 'calendar.events' => ['de' => 'Termine', 'en' => 'Events'], + 'calendar.no_events' => ['de' => 'Keine Termine', 'en' => 'No events'], + + // ── Event Form ────────────────────────── + 'events.title' => ['de' => 'Titel', 'en' => 'Title'], + 'events.title_placeholder' => ['de' => 'Titel hinzufΓΌgen', 'en' => 'Add title'], + 'events.datetime' => ['de' => 'Datum & Zeit', 'en' => 'Date & Time'], + 'events.date' => ['de' => 'Datum', 'en' => 'Date'], + 'events.multiday' => ['de' => 'MehrtΓ€gig', 'en' => 'Multi-day'], + 'events.custom_days' => ['de' => 'Einzelne Tage anpassen', 'en' => 'Customize individual days'], + 'events.custom' => ['de' => 'Custom', 'en' => 'Custom'], + 'events.standard' => ['de' => 'Standard', 'en' => 'Standard'], + 'events.time' => ['de' => 'Zeit', 'en' => 'Time'], + 'events.all_day' => ['de' => 'GanztΓ€gig', 'en' => 'All day'], + 'events.color' => ['de' => 'Farbe', 'en' => 'Color'], + 'events.participants' => ['de' => 'Teilnehmer', 'en' => 'Participants'], + 'events.note' => ['de' => 'Notiz', 'en' => 'Note'], + 'events.note_placeholder' => ['de' => 'Notizen zum Termin...', 'en' => 'Notes for this event...'], + 'events.standard_all_days' => ['de' => 'Standard (fΓΌr alle Tage)', 'en' => 'Standard (for all days)'], + 'events.start' => ['de' => 'Start', 'en' => 'Start'], + 'events.end' => ['de' => 'Ende', 'en' => 'End'], + 'events.adjust_day_only' => ['de' => 'Nur diesen Tag anpassen', 'en' => 'Adjust this day only'], + 'events.overrides_defaults' => ['de' => 'Überschreibt die Standardwerte', 'en' => 'Overrides default values'], + 'events.changes_this_day' => ['de' => 'Γ„nderungen betreffen nur diesen Tag.', 'en' => 'Changes apply to this day only.'], + + // ── Tasks ─────────────────────────────── + 'tasks.title' => ['de' => 'Aufgaben', 'en' => 'Tasks'], + 'tasks.subtitle' => ['de' => 'Todos und Aufgaben verwalten', 'en' => 'Manage todos and tasks'], + 'tasks.new' => ['de' => 'Neue Aufgabe', 'en' => 'New task'], + 'tasks.total' => ['de' => 'Gesamt', 'en' => 'Total'], + 'tasks.open' => ['de' => 'Offen', 'en' => 'Open'], + 'tasks.today' => ['de' => 'Heute', 'en' => 'Today'], + 'tasks.done' => ['de' => 'Erledigt', 'en' => 'Done'], + 'tasks.search' => ['de' => 'Aufgaben durchsuchen…', 'en' => 'Search tasks…'], + 'tasks.all' => ['de' => 'Alle', 'en' => 'All'], + 'tasks.in_progress' => ['de' => 'In Bearbeitung', 'en' => 'In progress'], + 'tasks.due_today' => ['de' => 'Heute fΓ€llig', 'en' => 'Due today'], + 'tasks.edit' => ['de' => 'Aufgabe bearbeiten', 'en' => 'Edit task'], + 'tasks.describe' => ['de' => 'Aufgabe beschreiben…', 'en' => 'Describe task…'], + 'tasks.description_optional' => ['de' => 'Beschreibung (optional)', 'en' => 'Description (optional)'], + 'tasks.priority' => ['de' => 'PrioritΓ€t', 'en' => 'Priority'], + 'tasks.priority_low' => ['de' => 'Niedrig', 'en' => 'Low'], + 'tasks.priority_medium' => ['de' => 'Mittel', 'en' => 'Medium'], + 'tasks.priority_high' => ['de' => 'Hoch', 'en' => 'High'], + 'tasks.due_at' => ['de' => 'FΓ€llig am', 'en' => 'Due on'], + 'tasks.no_tasks' => ['de' => 'Keine Aufgaben gefunden', 'en' => 'No tasks found'], + 'tasks.create_first' => ['de' => 'Erste Aufgabe erstellen', 'en' => 'Create first task'], + 'tasks.confirm_delete' => ['de' => 'Aufgabe lΓΆschen?', 'en' => 'Delete task?'], + 'tasks.to_in_progress' => ['de' => 'β†’ In Bearbeitung', 'en' => 'β†’ In progress'], + + // ── Notes ─────────────────────────────── + 'notes.color.yellow' => ['de' => 'Gelb', 'en' => 'Yellow'], + 'notes.color.blue' => ['de' => 'Blau', 'en' => 'Blue'], + 'notes.color.green' => ['de' => 'GrΓΌn', 'en' => 'Green'], + 'notes.color.pink' => ['de' => 'Rosa', 'en' => 'Pink'], + 'notes.color.purple' => ['de' => 'Lila', 'en' => 'Purple'], + 'notes.color.gray' => ['de' => 'Grau', 'en' => 'Gray'], + 'notes.title' => ['de' => 'Notizen', 'en' => 'Notes'], + 'notes.subtitle' => ['de' => 'Schnelle Gedanken und Ideen festhalten', 'en' => 'Capture quick thoughts and ideas'], + 'notes.new' => ['de' => 'Neue Notiz', 'en' => 'New note'], + 'notes.total' => ['de' => 'Notizen gesamt', 'en' => 'Total notes'], + 'notes.pinned' => ['de' => 'Angepinnt', 'en' => 'Pinned'], + 'notes.search' => ['de' => 'Notizen durchsuchen…', 'en' => 'Search notes…'], + 'notes.edit' => ['de' => 'Notiz bearbeiten', 'en' => 'Edit note'], + 'notes.title_placeholder' => ['de' => 'Titel (optional)', 'en' => 'Title (optional)'], + 'notes.content_placeholder' => ['de' => 'Notiz eingeben…', 'en' => 'Enter note…'], + 'notes.color_label' => ['de' => 'Farbe:', 'en' => 'Color:'], + 'notes.no_notes' => ['de' => 'Noch keine Notizen', 'en' => 'No notes yet'], + 'notes.create_first' => ['de' => 'Erste Notiz erstellen', 'en' => 'Create first note'], + 'notes.unpin' => ['de' => 'LoslΓΆsen', 'en' => 'Unpin'], + 'notes.pin' => ['de' => 'Anpinnen', 'en' => 'Pin'], + 'notes.confirm_delete' => ['de' => 'Notiz lΓΆschen?', 'en' => 'Delete note?'], + + // ── Contacts ──────────────────────────── + 'contacts.edit' => ['de' => 'Kontakt bearbeiten', 'en' => 'Edit contact'], + 'contacts.new' => ['de' => 'Neuer Kontakt', 'en' => 'New contact'], + 'contacts.name' => ['de' => 'Name', 'en' => 'Name'], + 'contacts.name_placeholder' => ['de' => 'Vor- und Nachname', 'en' => 'First and last name'], + 'contacts.email' => ['de' => 'E-Mail', 'en' => 'Email'], + 'contacts.email_placeholder' => ['de' => 'name@beispiel.de', 'en' => 'name@example.com'], + 'contacts.phone' => ['de' => 'Telefon', 'en' => 'Phone'], + 'contacts.phone_placeholder' => ['de' => '+49 123 456789', 'en' => '+1 234 567890'], + 'contacts.category' => ['de' => 'Kategorie', 'en' => 'Category'], + 'contacts.cat_private' => ['de' => 'Privat', 'en' => 'Private'], + 'contacts.cat_work' => ['de' => 'Arbeit', 'en' => 'Work'], + 'contacts.cat_customer' => ['de' => 'Kunde', 'en' => 'Customer'], + 'contacts.cat_other' => ['de' => 'Sonstiges', 'en' => 'Other'], + 'contacts.notes' => ['de' => 'Notizen', 'en' => 'Notes'], + 'contacts.notes_placeholder' => ['de' => 'Interne Notizen zum Kontakt…', 'en' => 'Internal notes about this contact…'], + 'contacts.title' => ['de' => 'Kontakte', 'en' => 'Contacts'], + 'contacts.subtitle' => ['de' => 'Verwalte deine Kontakte und GesprΓ€chspartner', 'en' => 'Manage your contacts and communication partners'], + 'contacts.total' => ['de' => 'Gesamt', 'en' => 'Total'], + 'contacts.customers' => ['de' => 'Kunden', 'en' => 'Customers'], + 'contacts.search' => ['de' => 'Name, E-Mail oder Telefon suchen…', 'en' => 'Search name, email or phone…'], + 'contacts.no_contacts' => ['de' => 'Keine Kontakte gefunden', 'en' => 'No contacts found'], + 'contacts.create_first' => ['de' => 'Ersten Kontakt erstellen', 'en' => 'Create first contact'], + 'contacts.confirm_delete' => ['de' => 'Kontakt \':name\' lΓΆschen?', 'en' => 'Delete contact \':name\'?'], + 'contacts.of_total' => ['de' => 'von :total Kontakten', 'en' => 'of :total contacts'], + + // ── Activities ────────────────────────── + 'activities.filter.all' => ['de' => 'Alle', 'en' => 'All'], + 'activities.filter.events' => ['de' => 'Termine', 'en' => 'Events'], + 'activities.filter.reminders' => ['de' => 'Erinnerungen', 'en' => 'Reminders'], + 'activities.filter.automations' => ['de' => 'Automationen', 'en' => 'Automations'], + 'activities.filter.contacts' => ['de' => 'Kontakte', 'en' => 'Contacts'], + 'activities.filter.notes' => ['de' => 'Notizen', 'en' => 'Notes'], + 'activities.filter.tasks' => ['de' => 'Aufgaben', 'en' => 'Tasks'], + 'activities.filter.integrations' => ['de' => 'Integrationen', 'en' => 'Integrations'], + 'activities.filter.system' => ['de' => 'System', 'en' => 'System'], + 'activities.title' => ['de' => 'AktivitΓ€ten', 'en' => 'Activities'], + 'activities.subtitle' => ['de' => 'VollstΓ€ndiges Protokoll aller Aktionen in deinem Konto', 'en' => 'Complete log of all actions in your account'], + 'activities.confirm_delete_all' => ['de' => 'Alle AktivitΓ€ten lΓΆschen?', 'en' => 'Delete all activities?'], + 'activities.delete_all' => ['de' => 'Alle lΓΆschen', 'en' => 'Delete all'], + 'activities.total' => ['de' => 'Gesamt', 'en' => 'Total'], + 'activities.today' => ['de' => 'Heute', 'en' => 'Today'], + 'activities.events' => ['de' => 'Termine', 'en' => 'Events'], + 'activities.automations' => ['de' => 'Automationen', 'en' => 'Automations'], + 'activities.search' => ['de' => 'AktivitΓ€ten durchsuchen…', 'en' => 'Search activities…'], + 'activities.no_activities' => ['de' => 'Keine AktivitΓ€ten gefunden', 'en' => 'No activities found'], + 'activities.adjust_filter' => ['de' => 'Passe die Suche oder den Filter an', 'en' => 'Adjust your search or filter'], + 'activities.auto_logged' => ['de' => 'Aktionen werden hier automatisch protokolliert', 'en' => 'Actions are automatically logged here'], + 'activities.yesterday' => ['de' => 'Gestern', 'en' => 'Yesterday'], + 'activities.of_total' => ['de' => 'von :total EintrΓ€gen', 'en' => 'of :total entries'], + + // ── Settings ──────────────────────────── + 'settings.title' => ['de' => 'Einstellungen', 'en' => 'Settings'], + 'settings.subtitle' => ['de' => 'Verwalte dein Konto, Benachrichtigungen und E-Mail-Konfiguration', 'en' => 'Manage your account, notifications and email configuration'], + 'settings.tab.profile' => ['de' => 'Profil', 'en' => 'Profile'], + 'settings.tab.security' => ['de' => 'Sicherheit', 'en' => 'Security'], + 'settings.tab.notifications' => ['de' => 'Benachrichtigungen', 'en' => 'Notifications'], + 'settings.tab.smtp' => ['de' => 'E-Mail (SMTP)', 'en' => 'Email (SMTP)'], + 'settings.tab.credits' => ['de' => 'Credits', 'en' => 'Credits'], + 'settings.tab.account' => ['de' => 'Konto', 'en' => 'Account'], + + // Settings: Profile + 'settings.profile.title' => ['de' => 'Profil', 'en' => 'Profile'], + 'settings.profile.subtitle' => ['de' => 'Name, E-Mail-Adresse und Regionaleinstellungen', 'en' => 'Name, email address and regional settings'], + 'settings.profile.name' => ['de' => 'Name', 'en' => 'Name'], + 'settings.profile.name_ph' => ['de' => 'Max Mustermann', 'en' => 'John Doe'], + 'settings.profile.email' => ['de' => 'E-Mail-Adresse', 'en' => 'Email address'], + 'settings.profile.email_ph' => ['de' => 'max@beispiel.de', 'en' => 'john@example.com'], + 'settings.profile.timezone' => ['de' => 'Zeitzone', 'en' => 'Timezone'], + 'settings.profile.language' => ['de' => 'Sprache', 'en' => 'Language'], + 'settings.profile.save' => ['de' => 'Profil speichern', 'en' => 'Save profile'], + + // Settings: Security + 'settings.security.title' => ['de' => 'Passwort Γ€ndern', 'en' => 'Change password'], + 'settings.security.subtitle' => ['de' => 'Verwende mindestens 6 Zeichen', 'en' => 'Use at least 6 characters'], + 'settings.security.current' => ['de' => 'Aktuelles Passwort', 'en' => 'Current password'], + 'settings.security.new' => ['de' => 'Neues Passwort', 'en' => 'New password'], + 'settings.security.confirm' => ['de' => 'Passwort bestΓ€tigen', 'en' => 'Confirm password'], + 'settings.security.save' => ['de' => 'Passwort Γ€ndern', 'en' => 'Change password'], + + // Settings: Notifications + 'settings.notif.title' => ['de' => 'Benachrichtigungen', 'en' => 'Notifications'], + 'settings.notif.subtitle' => ['de' => 'Steuere wie und wann du benachrichtigt wirst', 'en' => 'Control how and when you get notified'], + 'settings.notif.reminders' => ['de' => 'Erinnerungen aktivieren', 'en' => 'Enable reminders'], + 'settings.notif.reminders_desc' => ['de' => 'Schaltet alle Termin-Erinnerungen ein oder aus', 'en' => 'Turns all event reminders on or off'], + 'settings.notif.in_app' => ['de' => 'In-App Benachrichtigungen', 'en' => 'In-app notifications'], + 'settings.notif.in_app_desc' => ['de' => 'Benachrichtigungen direkt in der App anzeigen', 'en' => 'Show notifications directly in the app'], + 'settings.notif.email' => ['de' => 'E-Mail Benachrichtigungen', 'en' => 'Email notifications'], + 'settings.notif.email_desc' => ['de' => 'Erinnerungen und Agenda per E-Mail erhalten', 'en' => 'Receive reminders and agenda via email'], + 'settings.notif.save' => ['de' => 'Einstellungen speichern', 'en' => 'Save settings'], + + // Settings: SMTP + 'settings.smtp.title' => ['de' => 'E-Mail (SMTP)', 'en' => 'Email (SMTP)'], + 'settings.smtp.subtitle' => ['de' => 'Eigenen Mailserver fΓΌr Benachrichtigungen und Erinnerungen verwenden', 'en' => 'Use your own mail server for notifications and reminders'], + 'settings.smtp.host' => ['de' => 'SMTP Host', 'en' => 'SMTP Host'], + 'settings.smtp.host_ph' => ['de' => 'smtp.beispiel.de', 'en' => 'smtp.example.com'], + 'settings.smtp.port' => ['de' => 'Port', 'en' => 'Port'], + 'settings.smtp.username' => ['de' => 'Benutzername', 'en' => 'Username'], + 'settings.smtp.username_ph' => ['de' => 'user@beispiel.de', 'en' => 'user@example.com'], + 'settings.smtp.password' => ['de' => 'Passwort', 'en' => 'Password'], + 'settings.smtp.encryption' => ['de' => 'VerschlΓΌsselung', 'en' => 'Encryption'], + 'settings.smtp.none' => ['de' => 'Keine', 'en' => 'None'], + 'settings.smtp.test' => ['de' => 'Testmail senden', 'en' => 'Send test email'], + 'settings.smtp.sending' => ['de' => 'Sende…', 'en' => 'Sending…'], + 'settings.smtp.save' => ['de' => 'SMTP speichern', 'en' => 'Save SMTP'], + + 'settings.smtp.info_title' => ['de' => 'WofΓΌr ist eigener SMTP?', 'en' => 'What is custom SMTP for?'], + 'settings.smtp.info_body' => ['de' => 'Verbinde deinen eigenen Mailserver damit Aria E-Mails in deinem Namen versenden kann. Zum Beispiel: "Schicke eine TerminbestΓ€tigung an Sara von meiner Adresse."', 'en' => 'Connect your own mail server so Aria can send emails on your behalf. For example: "Send an appointment confirmation to Sara from my address."'], + 'settings.smtp.info_note' => ['de' => 'System-Benachrichtigungen (Erinnerungen, Agenda etc.) werden immer von reminder@aziros.com gesendet.', 'en' => 'System notifications (reminders, agenda etc.) are always sent from reminder@aziros.com.'], + + 'settings.smtp.rate_title' => ['de' => 'Sendelimit', 'en' => 'Send limit'], + 'settings.smtp.rate_desc' => ['de' => 'Maximal 10 E-Mails pro Minute ΓΌber deinen SMTP Server', 'en' => 'Maximum 10 emails per minute via your SMTP server'], + 'settings.smtp.today_sent' => ['de' => 'heute gesendet', 'en' => 'sent today'], + + 'settings.smtp.test_title' => ['de' => 'Verbindung testen', 'en' => 'Test connection'], + 'settings.smtp.test_desc' => ['de' => 'Sendet eine Test-Mail an :email', 'en' => 'Sends a test email to :email'], + 'settings.smtp.test_send' => ['de' => 'Test senden', 'en' => 'Send test'], + 'settings.smtp.test_sending' => ['de' => 'Wird gesendet...', 'en' => 'Sending...'], + 'settings.smtp.test_success' => ['de' => 'Test-Mail erfolgreich gesendet', 'en' => 'Test email sent successfully'], + 'settings.smtp.test_error' => ['de' => 'Fehler: :error', 'en' => 'Error: :error'], + + // Settings: Credits + 'settings.credits.plan_limit' => ['de' => 'Plan-Limit (monatlich)', 'en' => 'Plan limit (monthly)'], + 'settings.credits.resets' => ['de' => 'resettet am 1. jeden Monats', 'en' => 'resets on the 1st of each month'], + 'settings.credits.balance' => ['de' => 'Guthaben (kumuliert)', 'en' => 'Balance (cumulative)'], + 'settings.credits.no_expire' => ['de' => 'lΓ€uft nicht ab', 'en' => 'does not expire'], + 'settings.credits.effective' => ['de' => 'Effektives Limit diesen Monat', 'en' => 'Effective limit this month'], + 'settings.credits.plan_plus' => ['de' => 'Plan + Guthaben', 'en' => 'Plan + Balance'], + 'settings.credits.usage' => ['de' => 'Verbrauch diesen Monat', 'en' => 'Usage this month'], + 'settings.credits.balance_note' => ['de' => 'Erst wenn :limit Plan-Credits verbraucht sind, wird dein Guthaben verwendet.', 'en' => 'Your balance will only be used after :limit plan credits are consumed.'], + 'settings.credits.history' => ['de' => 'Credit-Verlauf', 'en' => 'Credit history'], + 'settings.credits.last_20' => ['de' => 'Letzte 20 Transaktionen', 'en' => 'Last 20 transactions'], + 'settings.credits.no_transactions' => ['de' => 'Noch keine Transaktionen vorhanden', 'en' => 'No transactions yet'], + 'settings.credits.type.welcome' => ['de' => 'Willkommen', 'en' => 'Welcome'], + 'settings.credits.type.affiliate' => ['de' => 'Affiliate', 'en' => 'Affiliate'], + 'settings.credits.type.gift' => ['de' => 'Geschenk', 'en' => 'Gift'], + 'settings.credits.type.refund' => ['de' => 'RΓΌckerstattung', 'en' => 'Refund'], + 'settings.credits.type.sub_bonus' => ['de' => 'Abo-Bonus', 'en' => 'Sub bonus'], + 'settings.credits.type.usage' => ['de' => 'Verbrauch', 'en' => 'Usage'], + + // Settings: Affiliate + 'settings.affiliate.title' => ['de' => 'Affiliate-Programm', 'en' => 'Affiliate Program'], + 'settings.affiliate.subtitle' => ['de' => 'Lade Freunde ein und erhalte Credits wenn sie 3 Monate dabei bleiben', 'en' => 'Invite friends and earn credits when they stay for 3 months'], + 'settings.affiliate.invited' => ['de' => 'Eingeladen', 'en' => 'Invited'], + 'settings.affiliate.qualified' => ['de' => 'Qualifiziert', 'en' => 'Qualified'], + 'settings.affiliate.earned' => ['de' => 'Credits verdient', 'en' => 'Credits earned'], + 'settings.affiliate.link' => ['de' => 'Dein persΓΆnlicher Einladungslink', 'en' => 'Your personal referral link'], + 'settings.affiliate.copy' => ['de' => 'Kopieren', 'en' => 'Copy'], + 'settings.affiliate.copied' => ['de' => 'Kopiert', 'en' => 'Copied'], + 'settings.affiliate.code' => ['de' => 'Dein Code:', 'en' => 'Your code:'], + 'settings.affiliate.reward' => ['de' => 'FΓΌr jeden Freund der 3 Monate bleibt erhΓ€ltst du', 'en' => 'For every friend who stays for 3 months you receive'], + 'settings.affiliate.users' => ['de' => 'Eingeladene Nutzer', 'en' => 'Invited users'], + 'settings.affiliate.registered' => ['de' => 'Registriert', 'en' => 'Registered'], + 'settings.affiliate.qualified_at' => ['de' => 'Qualifiziert am', 'en' => 'Qualified on'], + 'settings.affiliate.status.pending' => ['de' => 'Ausstehend', 'en' => 'Pending'], + 'settings.affiliate.status.qualified' => ['de' => 'Qualifiziert', 'en' => 'Qualified'], + 'settings.affiliate.status.credited' => ['de' => 'Gutgeschrieben', 'en' => 'Credited'], + 'settings.affiliate.status.cancelled' => ['de' => 'Storniert', 'en' => 'Cancelled'], + 'settings.affiliate.status.unknown' => ['de' => 'Unbekannt', 'en' => 'Unknown'], + 'settings.affiliate.not_member' => ['de' => 'Du nimmst noch nicht am Affiliate-Programm teil.', 'en' => 'You are not yet part of the affiliate program.'], + 'settings.affiliate.join_cta' => ['de' => 'Lade Freunde ein und erhalte :credits fΓΌr jeden, der 3 Monate dabei bleibt.', 'en' => 'Invite friends and earn :credits for each one who stays for 3 months.'], + 'settings.affiliate.join' => ['de' => 'Jetzt teilnehmen', 'en' => 'Join now'], + + // Settings: App (Mobile) + 'settings.credits.last_30' => ['de' => 'Letzte 30 Transaktionen', 'en' => 'Last 30 transactions'], + 'settings.credits.type.event' => ['de' => 'Termin', 'en' => 'Event'], + 'settings.credits.type.note' => ['de' => 'Notiz', 'en' => 'Note'], + 'settings.credits.type.task' => ['de' => 'Aufgabe', 'en' => 'Task'], + 'settings.credits.type.chat' => ['de' => 'Chat', 'en' => 'Chat'], + 'settings.credits.type.contact' => ['de' => 'Kontakt', 'en' => 'Contact'], + 'settings.credits.type.email' => ['de' => 'E-Mail', 'en' => 'Email'], + 'settings.credits.type.multi' => ['de' => 'Multi', 'en' => 'Multi'], + 'settings.credits.type.subscription' => ['de' => 'Abo', 'en' => 'Subscription'], + 'settings.unlimited' => ['de' => 'Unbegrenzt', 'en' => 'Unlimited'], + 'settings.alert.logout_title' => ['de' => 'Abmelden', 'en' => 'Sign out'], + 'settings.alert.logout_confirm' => ['de' => 'Wirklich abmelden?', 'en' => 'Are you sure you want to sign out?'], + 'settings.alert.delete_title' => ['de' => 'Konto lΓΆschen', 'en' => 'Delete account'], + 'settings.alert.delete_confirm' => ['de' => 'Alle Daten werden dauerhaft gelΓΆscht. Diese Aktion kann nicht rΓΌckgΓ€ngig gemacht werden.', 'en' => 'All data will be permanently deleted. This action cannot be undone.'], + 'settings.alert.delete_error' => ['de' => 'Konto konnte nicht gelΓΆscht werden.', 'en' => 'Could not delete account.'], + 'settings.alert.profile_error' => ['de' => 'Profil konnte nicht gespeichert werden.', 'en' => 'Could not save profile.'], + 'settings.alert.password_changed' => ['de' => 'Passwort wurde geΓ€ndert.', 'en' => 'Password has been changed.'], + 'settings.alert.password_error' => ['de' => 'Passwort konnte nicht geΓ€ndert werden.', 'en' => 'Could not change password.'], + 'settings.alert.password_mismatch' => ['de' => 'PasswΓΆrter stimmen nicht ΓΌberein.', 'en' => 'Passwords do not match.'], + 'settings.alert.password_min' => ['de' => 'Mindestens 6 Zeichen.', 'en' => 'At least 6 characters.'], + 'settings.alert.language_title' => ['de' => 'Sprache', 'en' => 'Language'], + 'settings.alert.language_select' => ['de' => 'Sprache auswΓ€hlen:', 'en' => 'Select language:'], + 'settings.alert.timezone_title' => ['de' => 'Zeitzone', 'en' => 'Timezone'], + 'settings.alert.timezone_info' => ['de' => 'Aktuelle Zeitzone: :tz\n\nZeitzone kann auf app.aziros.com geΓ€ndert werden.', 'en' => 'Current timezone: :tz\n\nTimezone can be changed on app.aziros.com.'], + 'settings.share_text' => ['de' => 'Probier aziros aus!', 'en' => 'Try aziros!'], + 'settings.account_section' => ['de' => 'Konto', 'en' => 'Account'], + 'settings.invited_count' => ['de' => ':count eingeladen Β· :credits Credits verdient', 'en' => ':count invited Β· :credits credits earned'], + 'settings.placeholder.name' => ['de' => 'Dein Name', 'en' => 'Your name'], + + // Settings: Account / Danger + 'settings.account.title' => ['de' => 'Konto & Datenschutz', 'en' => 'Account & Privacy'], + 'settings.account.subtitle' => ['de' => 'Unwiderrufliche Aktionen β€” bitte mit Bedacht nutzen', 'en' => 'Irreversible actions β€” please use with caution'], + 'settings.account.delete' => ['de' => 'Konto lΓΆschen', 'en' => 'Delete account'], + 'settings.account.delete_desc' => ['de' => 'Alle Daten (Termine, Kontakte, AktivitΓ€ten, Einstellungen) werden dauerhaft gelΓΆscht. Diese Aktion ist nicht rΓΌckgΓ€ngig zu machen.', 'en' => 'All data (events, contacts, activities, settings) will be permanently deleted. This action cannot be undone.'], + + // ── Subscription ──────────────────────── + 'subscription.title' => ['de' => 'Abonnement', 'en' => 'Subscription'], + 'subscription.subtitle' => ['de' => 'Verwalte deinen Plan, upgrade jederzeit', 'en' => 'Manage your plan, upgrade anytime'], + 'subscription.current_plan' => ['de' => 'Aktueller Plan', 'en' => 'Current Plan'], + 'subscription.yearly' => ['de' => 'JΓ€hrlich', 'en' => 'Yearly'], + 'subscription.monthly' => ['de' => 'Monatlich', 'en' => 'Monthly'], + 'subscription.duration' => ['de' => 'Laufzeit', 'en' => 'Duration'], + 'subscription.active_since' => ['de' => 'Aktiv seit', 'en' => 'Active since'], + 'subscription.ends_at' => ['de' => 'Endet am', 'en' => 'Ends on'], + 'subscription.renews_at' => ['de' => 'VerlΓ€ngert am', 'en' => 'Renews on'], + 'subscription.credits_month' => ['de' => 'Credits / Monat', 'en' => 'Credits / Month'], + 'subscription.no_plan' => ['de' => 'Noch kein aktiver Plan', 'en' => 'No active plan yet'], + 'subscription.no_plan_hint' => ['de' => 'WΓ€hle einen Plan um loszulegen', 'en' => 'Choose a plan to get started'], + 'subscription.internal' => ['de' => 'Dein Plan wird intern verwaltet und kann nicht geΓ€ndert werden.', 'en' => 'Your plan is managed internally and cannot be changed.'], + 'subscription.change_plan' => ['de' => 'Plan wechseln', 'en' => 'Change plan'], + 'subscription.save_months' => ['de' => 'spare 2 Mon.', 'en' => 'save 2 mo.'], + 'subscription.popular' => ['de' => 'Beliebt', 'en' => 'Popular'], + 'subscription.your_plan' => ['de' => 'Dein Plan', 'en' => 'Your Plan'], + 'subscription.free' => ['de' => 'Kostenlos', 'en' => 'Free'], + 'subscription.per_month' => ['de' => 'pro Monat', 'en' => 'per month'], + 'subscription.billed_yearly' => ['de' => 'jΓ€hrlich', 'en' => 'yearly'], + 'subscription.months_free' => ['de' => ':count Monate gratis', 'en' => ':count months free'], + 'subscription.current_active' => ['de' => 'Aktuell aktiv', 'en' => 'Currently active'], + 'subscription.downgrade' => ['de' => 'Downgrade', 'en' => 'Downgrade'], + 'subscription.features' => ['de' => 'Features', 'en' => 'Features'], + 'subscription.current' => ['de' => 'Aktuell', 'en' => 'Current'], + 'subscription.switch' => ['de' => 'Wechseln', 'en' => 'Switch'], + 'subscription.cancel_anytime' => ['de' => 'Jederzeit kΓΌndbar', 'en' => 'Cancel anytime'], + 'subscription.no_hidden_costs' => ['de' => 'Keine versteckten Kosten', 'en' => 'No hidden costs'], + 'subscription.instant_active' => ['de' => 'Sofort aktiv', 'en' => 'Instantly active'], + + // ── Integrations ──────────────────────── + 'integrations.title' => ['de' => 'Integrationen', 'en' => 'Integrations'], + 'integrations.subtitle' => ['de' => 'Verbinde externe Kalender-Dienste mit deinem Konto', 'en' => 'Connect external calendar services with your account'], + 'integrations.connected_msg' => ['de' => 'wurde erfolgreich verbunden.', 'en' => 'was successfully connected.'], + 'integrations.calendar' => ['de' => 'Kalender', 'en' => 'Calendar'], + 'integrations.google' => ['de' => 'Google Kalender', 'en' => 'Google Calendar'], + 'integrations.outlook' => ['de' => 'Outlook Kalender', 'en' => 'Outlook Calendar'], + 'integrations.not_connected' => ['de' => 'Noch nicht verbunden', 'en' => 'Not connected yet'], + 'integrations.connected' => ['de' => 'Verbunden', 'en' => 'Connected'], + 'integrations.disconnected' => ['de' => 'Nicht verbunden', 'en' => 'Not connected'], + 'integrations.read_only' => ['de' => 'Nur lesen', 'en' => 'Read only'], + 'integrations.write_only' => ['de' => 'Nur schreiben', 'en' => 'Write only'], + 'integrations.bidirectional' => ['de' => 'Beidseitig', 'en' => 'Bidirectional'], + 'integrations.manage' => ['de' => 'Verwalten', 'en' => 'Manage'], + 'integrations.pro_required' => ['de' => 'Pro erforderlich', 'en' => 'Pro required'], + + // Integration Provider Modal + 'integrations.token_expired' => ['de' => 'Token abgelaufen', 'en' => 'Token expired'], + 'integrations.reconnect_hint' => ['de' => 'Bitte neu verbinden', 'en' => 'Please reconnect'], + 'integrations.sync_mode' => ['de' => 'Synchronisierungsmodus', 'en' => 'Sync mode'], + 'integrations.sync_pro_hint' => ['de' => 'Sync-Modi sind nur im Pro-Plan verfΓΌgbar.', 'en' => 'Sync modes are only available on the Pro plan.'], + 'integrations.last_sync' => ['de' => 'Letzter Sync:', 'en' => 'Last sync:'], + 'integrations.not_synced' => ['de' => 'Noch nicht synchronisiert', 'en' => 'Not synced yet'], + 'integrations.sync_now' => ['de' => 'Jetzt sync', 'en' => 'Sync now'], + 'integrations.syncing' => ['de' => 'LΓ€dt…', 'en' => 'Syncing…'], + 'integrations.push_active' => ['de' => 'Push-Benachrichtigungen aktiv', 'en' => 'Push notifications active'], + 'integrations.push_expires' => ['de' => 'lΓ€uft ab', 'en' => 'expires'], + 'integrations.push_inactive' => ['de' => 'Push-Benachrichtigungen inaktiv', 'en' => 'Push notifications inactive'], + 'integrations.activate' => ['de' => 'Aktivieren', 'en' => 'Activate'], + 'integrations.connected_since' => ['de' => 'Verbunden seit', 'en' => 'Connected since'], + 'integrations.connect_desc' => ['de' => 'Verbinde :name, um deine Termine automatisch zu importieren und zu synchronisieren.', 'en' => 'Connect :name to automatically import and sync your events.'], + 'integrations.free_hint' => ['de' => 'Im kostenlosen Plan werden Termine nur gelesen. FΓΌr beidseitige Synchronisierung ist der', 'en' => 'On the free plan, events are read-only. For bidirectional sync, the'], + 'integrations.pro_plan' => ['de' => 'Pro-Plan', 'en' => 'Pro plan'], + 'integrations.required' => ['de' => 'erforderlich.', 'en' => 'is required.'], + 'integrations.confirm_disconnect' => ['de' => 'Verbindung wirklich trennen?', 'en' => 'Really disconnect?'], + 'integrations.disconnect_btn' => ['de' => 'Verbindung trennen', 'en' => 'Disconnect'], + 'integrations.reconnect' => ['de' => 'Neu verbinden', 'en' => 'Reconnect'], + + // ── Automations ───────────────────────── + 'automations.title' => ['de' => 'Automationen', 'en' => 'Automations'], + 'automations.subtitle' => ['de' => 'Richte automatische Aktionen fΓΌr deinen Kalender ein', 'en' => 'Set up automatic actions for your calendar'], + 'automations.unlock' => ['de' => 'Automationen freischalten', 'en' => 'Unlock automations'], + 'automations.upgrade_hint' => ['de' => 'Upgrade auf Pro fΓΌr :count+ Automation-Vorlagen', 'en' => 'Upgrade to Pro for :count+ automation templates'], + 'automations.upgrade_now' => ['de' => 'Jetzt upgraden', 'en' => 'Upgrade now'], + 'automations.unlock_all' => ['de' => 'Alle Automationen freischalten', 'en' => 'Unlock all automations'], + 'automations.cancel_anytime' => ['de' => 'Jederzeit kΓΌndbar Β· Keine versteckten Kosten', 'en' => 'Cancel anytime Β· No hidden costs'], + 'automations.channel.email' => ['de' => 'E-Mail', 'en' => 'Email'], + 'automations.channel.both' => ['de' => 'Beide', 'en' => 'Both'], + 'automations.day.monday' => ['de' => 'Montag', 'en' => 'Monday'], + 'automations.day.tuesday' => ['de' => 'Dienstag', 'en' => 'Tuesday'], + 'automations.day.wednesday' => ['de' => 'Mittwoch', 'en' => 'Wednesday'], + 'automations.day.thursday' => ['de' => 'Donnerstag', 'en' => 'Thursday'], + 'automations.day.friday' => ['de' => 'Freitag', 'en' => 'Friday'], + 'automations.day.saturday' => ['de' => 'Samstag', 'en' => 'Saturday'], + 'automations.day.sunday' => ['de' => 'Sonntag', 'en' => 'Sunday'], + 'automations.hours_before' => ['de' => 'Std. vorher', 'en' => 'hrs before'], + 'automations.minutes_before' => ['de' => 'Min. vorher', 'en' => 'min before'], + 'automations.oclock' => ['de' => 'Uhr', 'en' => 'o\'clock'], + 'automations.min_after' => ['de' => 'Min. nach Termin', 'en' => 'min after event'], + 'automations.days_before' => ['de' => 'Tage vorher', 'en' => 'days before'], + 'automations.after_free_days' => ['de' => 'Nach ... freien Tagen', 'en' => 'After ... free days'], + 'automations.plus_activities' => ['de' => '+ AktivitΓ€ten', 'en' => '+ Activities'], + 'automations.after_days' => ['de' => 'Nach ... Tagen', 'en' => 'After ... days'], + 'automations.from_min' => ['de' => 'ab ... Min.', 'en' => 'from ... min'], + 'automations.last_run' => ['de' => 'Zuletzt:', 'en' => 'Last run:'], + 'automations.confirm_reset' => ['de' => 'Automation zurΓΌcksetzen?', 'en' => 'Reset automation?'], + 'automations.reset' => ['de' => 'ZurΓΌcksetzen', 'en' => 'Reset'], + 'automations.configure' => ['de' => 'Automation konfigurieren', 'en' => 'Configure automation'], + 'automations.channel_label' => ['de' => 'Benachrichtigungskanal', 'en' => 'Notification channel'], + 'automations.how_early' => ['de' => 'Wie frΓΌh erinnern?', 'en' => 'How early to remind?'], + 'automations.time.15min' => ['de' => '15 Minuten', 'en' => '15 minutes'], + 'automations.time.30min' => ['de' => '30 Minuten', 'en' => '30 minutes'], + 'automations.time.1h' => ['de' => '1 Stunde', 'en' => '1 hour'], + 'automations.time.2h' => ['de' => '2 Stunden', 'en' => '2 hours'], + 'automations.time.1d' => ['de' => '1 Tag', 'en' => '1 day'], + 'automations.time_label' => ['de' => 'Uhrzeit', 'en' => 'Time'], + 'automations.weekdays_only' => ['de' => 'Nur Werktage', 'en' => 'Weekdays only'], + 'automations.weekdays_desc' => ['de' => 'Mo–Fr, Wochenende ΓΌberspringen', 'en' => 'Mon–Fri, skip weekends'], + 'automations.weekday' => ['de' => 'Wochentag', 'en' => 'Weekday'], + 'automations.delay_after' => ['de' => 'VerzΓΆgerung nach Terminende', 'en' => 'Delay after event ends'], + 'automations.delay.immediate' => ['de' => 'Sofort', 'en' => 'Immediately'], + 'automations.delay.30min' => ['de' => '30 Min.', 'en' => '30 min'], + 'automations.delay.1h' => ['de' => '1 Stunde', 'en' => '1 hour'], + 'automations.create_activity' => ['de' => 'AktivitΓ€t erstellen', 'en' => 'Create activity'], + 'automations.auto_after_event' => ['de' => 'Automatisch nach jedem Termin', 'en' => 'Automatically after each event'], + 'automations.days_before_label' => ['de' => 'Wie viele Tage vorher?', 'en' => 'How many days before?'], + 'automations.before.1d' => ['de' => '1 Tag', 'en' => '1 day'], + 'automations.before.3d' => ['de' => '3 Tage', 'en' => '3 days'], + 'automations.before.1w' => ['de' => '1 Woche', 'en' => '1 week'], + 'automations.before.2w' => ['de' => '2 Wochen', 'en' => '2 weeks'], + 'automations.free_period' => ['de' => 'Terminfrei-Zeitraum', 'en' => 'Event-free period'], + 'automations.period.1d' => ['de' => '1 Tag', 'en' => '1 day'], + 'automations.period.3d' => ['de' => '3 Tage', 'en' => '3 days'], + 'automations.period.7d' => ['de' => '7 Tage', 'en' => '7 days'], + 'automations.include_activities' => ['de' => 'AktivitΓ€ten einbeziehen', 'en' => 'Include activities'], + 'automations.include_desc' => ['de' => 'Zeigt auch AktivitΓ€ten, nicht nur Termine', 'en' => 'Also shows activities, not just events'], + 'automations.reminder_after' => ['de' => 'Erinnerung nach wie vielen Tagen?', 'en' => 'Reminder after how many days?'], + 'automations.after.1w' => ['de' => '1 Woche', 'en' => '1 week'], + 'automations.after.2w' => ['de' => '2 Wochen', 'en' => '2 weeks'], + 'automations.after.1m' => ['de' => '1 Monat', 'en' => '1 month'], + 'automations.after.3m' => ['de' => '3 Monate', 'en' => '3 months'], + 'automations.min_slot' => ['de' => 'Mindest-Zeitfenster', 'en' => 'Minimum time slot'], + 'automations.slot.30min' => ['de' => '30 Min.', 'en' => '30 min'], + 'automations.slot.1h' => ['de' => '1 Stunde', 'en' => '1 hour'], + 'automations.slot.2h' => ['de' => '2 Stunden', 'en' => '2 hours'], + 'automations.save_activate' => ['de' => 'Speichern & aktivieren', 'en' => 'Save & activate'], + 'automations.settings_row' => ['de' => 'Automationen', 'en' => 'Automations'], + 'automations.settings_sub' => ['de' => 'Automatische Aktionen konfigurieren', 'en' => 'Configure automatic actions'], + 'automations.active_count' => ['de' => ':count aktiv', 'en' => ':count active'], + 'automations.settings.send_time' => ['de' => 'Uhrzeit', 'en' => 'Time'], + 'automations.settings.minutes_before' => ['de' => 'Minuten vorher', 'en' => 'Minutes before'], + 'automations.settings.days_before' => ['de' => 'Tage vorher', 'en' => 'Days before'], + 'automations.settings.days_since_contact' => ['de' => 'Tage ohne Kontakt', 'en' => 'Days without contact'], + 'automations.settings.days_inactive' => ['de' => 'Tage ohne Termine', 'en' => 'Days without events'], + 'automations.settings.weekdays_only' => ['de' => 'Nur Werktage', 'en' => 'Weekdays only'], + 'automations.section.free' => ['de' => 'KOSTENLOS', 'en' => 'FREE'], + 'automations.configure_btn' => ['de' => 'Konfigurieren', 'en' => 'Configure'], + 'automations.chip.day' => ['de' => 'Tag', 'en' => 'day'], + 'automations.chip.days' => ['de' => 'Tage', 'en' => 'days'], + 'automations.chip.abbr_day' => ['de' => 'T', 'en' => 'D'], + 'automations.chip.abbr_month' => ['de' => 'M', 'en' => 'M'], + 'automations.save_error' => ['de' => 'Einstellungen konnten nicht gespeichert werden.', 'en' => 'Settings could not be saved.'], + 'automations.reset_error' => ['de' => 'ZurΓΌcksetzen fehlgeschlagen.', 'en' => 'Reset failed.'], + 'automations.before.today' => ['de' => 'Heute', 'en' => 'Today'], + 'automations.channel.in_app' => ['de' => 'In-App', 'en' => 'In-App'], + 'automations.settings.channel' => ['de' => 'Benachrichtigungskanal', 'en' => 'Notification channel'], + + // Typ-Namen und Beschreibungen + 'automations.type.event_reminder.name' => ['de' => 'Termin-Erinnerung', 'en' => 'Event Reminder'], + 'automations.type.event_reminder.description' => ['de' => 'Erinnert dich X Minuten/Stunden vor jedem Termin.', 'en' => 'Reminds you X minutes/hours before each event.'], + 'automations.type.daily_agenda.name' => ['de' => 'TΓ€gliche Agenda', 'en' => 'Daily Agenda'], + 'automations.type.daily_agenda.description' => ['de' => 'Sendet dir jeden Morgen eine Übersicht aller Termine.', 'en' => 'Sends you a daily overview of all events each morning.'], + 'automations.type.weekly_overview.name' => ['de' => 'WΓΆchentliche Vorschau', 'en' => 'Weekly Overview'], + 'automations.type.weekly_overview.description' => ['de' => 'Zusammenfassung der kommenden Woche jeden Montag.', 'en' => 'Summary of the upcoming week every Monday.'], + 'automations.type.event_followup.name' => ['de' => 'Termin-Nachbereitung', 'en' => 'Event Follow-up'], + 'automations.type.event_followup.description' => ['de' => 'Erstellt nach einem Termin automatisch eine AktivitΓ€t.', 'en' => 'Automatically creates an activity after each event.'], + 'automations.type.birthday_reminder.name' => ['de' => 'Geburtstags-Erinnerung', 'en' => 'Birthday Reminder'], + 'automations.type.birthday_reminder.description' => ['de' => 'Erinnert dich X Tage vor dem Geburtstag deiner Kontakte.', 'en' => 'Reminds you X days before your contacts\' birthdays.'], + 'automations.type.no_activity_reminder.name' => ['de' => 'Leere-Tage-Erinnerung', 'en' => 'Inactive Days Reminder'], + 'automations.type.no_activity_reminder.description' => ['de' => 'Benachrichtigt dich bei X aufeinanderfolgenden Tagen ohne Termine.', 'en' => 'Notifies you after X consecutive days without events.'], + 'automations.type.daily_summary.name' => ['de' => 'TagesrΓΌckblick', 'en' => 'Daily Summary'], + 'automations.type.daily_summary.description' => ['de' => 'Sendet dir abends eine Zusammenfassung des Tages.', 'en' => 'Sends you an evening summary of the day.'], + 'automations.type.contact_followup.name' => ['de' => 'Kontakt-Nachfassung', 'en' => 'Contact Follow-up'], + 'automations.type.contact_followup.description' => ['de' => 'Erinnert dich, wenn du einen Kontakt zu lange nicht kontaktiert hast.', 'en' => 'Reminds you when you haven\'t contacted someone in a while.'], + 'automations.type.free_slots_report.name' => ['de' => 'Freie-Zeiten-Bericht', 'en' => 'Free Slots Report'], + 'automations.type.free_slots_report.description' => ['de' => 'WΓΆchentliche Übersicht deiner freien Zeitfenster.', 'en' => 'Weekly overview of your available time slots.'], + + // ── Invoices ──────────────────────────── + 'invoices.title' => ['de' => 'Rechnungen', 'en' => 'Invoices'], + 'invoices.subtitle' => ['de' => 'Übersicht deiner Zahlungen und Abrechnung', 'en' => 'Overview of your payments and billing'], + 'invoices.manage_plan' => ['de' => 'Plan verwalten β†’', 'en' => 'Manage plan β†’'], + 'invoices.choose_plan' => ['de' => 'Plan auswΓ€hlen', 'en' => 'Choose a plan'], + 'invoices.current_sub' => ['de' => 'Aktuelles Abo', 'en' => 'Current subscription'], + 'invoices.next_billing' => ['de' => 'NΓ€chste Abrechnung', 'en' => 'Next billing'], + 'invoices.days' => ['de' => 'Tage', 'en' => 'Days'], + 'invoices.due_now' => ['de' => 'Jetzt fΓ€llig', 'en' => 'Due now'], + 'invoices.ends_at' => ['de' => 'Endet am', 'en' => 'Ends on'], + 'invoices.no_sub' => ['de' => 'Kein aktives Abonnement', 'en' => 'No active subscription'], + 'invoices.no_sub_hint' => ['de' => 'WΓ€hle einen Plan um loszulegen', 'en' => 'Choose a plan to get started'], + 'invoices.view_plans' => ['de' => 'PlΓ€ne ansehen', 'en' => 'View plans'], + 'invoices.payment_failed' => ['de' => 'Zahlung fehlgeschlagen', 'en' => 'Payment failed'], + 'invoices.payment_pending' => ['de' => 'Zahlung ausstehend', 'en' => 'Payment pending'], + 'invoices.due_since' => ['de' => 'FΓ€llig seit', 'en' => 'Due since'], + 'invoices.pay_now' => ['de' => 'Jetzt bezahlen', 'en' => 'Pay now'], + 'invoices.all' => ['de' => 'Alle Rechnungen', 'en' => 'All invoices'], + 'invoices.no_invoices' => ['de' => 'Noch keine Rechnungen vorhanden', 'en' => 'No invoices yet'], + + // ── Checkout ──────────────────────────── + 'checkout.switch_free' => ['de' => 'Zum Free Plan wechseln', 'en' => 'Switch to Free plan'], + 'checkout.cancel_immediately' => ['de' => 'Dein Abonnement wird sofort gekΓΌndigt', 'en' => 'Your subscription will be cancelled immediately'], + 'checkout.change_plan' => ['de' => 'Plan wechseln', 'en' => 'Change plan'], + 'checkout.prorated' => ['de' => 'Abrechnung wird anteilig angepasst', 'en' => 'Billing will be prorated'], + 'checkout.title' => ['de' => 'Checkout', 'en' => 'Checkout'], + 'checkout.switch_to' => ['de' => 'Plan wechseln zu', 'en' => 'Switch plan to'], + 'checkout.billing_period' => ['de' => 'Abrechnungszeitraum', 'en' => 'Billing period'], + 'checkout.monthly' => ['de' => 'Monatlich', 'en' => 'Monthly'], + 'checkout.per_month' => ['de' => 'pro Monat', 'en' => 'per month'], + 'checkout.yearly' => ['de' => 'JΓ€hrlich', 'en' => 'Yearly'], + 'checkout.months_free' => ['de' => 'Monate gratis', 'en' => 'months free'], + 'checkout.per_month_yearly' => ['de' => 'pro Monat, jΓ€hrlich abgerechnet', 'en' => 'per month, billed yearly'], + 'checkout.save_comparison' => ['de' => 'Du sparst :amount im Vergleich zu monatlicher Abrechnung.', 'en' => 'You save :amount compared to monthly billing.'], + 'checkout.included' => ['de' => 'Im :plan-Plan enthalten', 'en' => 'Included in :plan plan'], + 'checkout.all_features' => ['de' => 'Alle Funktionen der Plattform.', 'en' => 'All platform features.'], + 'checkout.credits_month' => ['de' => 'KI-Credits pro Monat', 'en' => 'AI credits per month'], + 'checkout.secure_payment' => ['de' => 'Sichere Zahlung', 'en' => 'Secure payment'], + 'checkout.ssl_stripe' => ['de' => 'SSL-verschlΓΌsselt via Stripe', 'en' => 'SSL encrypted via Stripe'], + 'checkout.cancel_anytime' => ['de' => 'Jederzeit kΓΌndbar', 'en' => 'Cancel anytime'], + 'checkout.no_minimum' => ['de' => 'Keine Mindestlaufzeit', 'en' => 'No minimum term'], + 'checkout.gdpr' => ['de' => 'DSGVO-konform', 'en' => 'GDPR compliant'], + 'checkout.eu_data' => ['de' => 'Daten in EU-Rechenzentren', 'en' => 'Data in EU data centers'], + 'checkout.stripe_note' => ['de' => 'Zahlungsabwicklung sicher durch Stripe. aziros speichert keine Kartendaten.', 'en' => 'Payment processing secured by Stripe. aziros does not store card data.'], + 'checkout.total' => ['de' => 'Gesamtbetrag', 'en' => 'Total amount'], + 'checkout.yearly_discount' => ['de' => 'Jahres-Rabatt', 'en' => 'Yearly discount'], + 'checkout.total_incl_vat' => ['de' => 'Gesamt inkl. MwSt.', 'en' => 'Total incl. VAT'], + 'checkout.auto_renew' => ['de' => 'Automatische VerlΓ€ngerung alle :period. Jederzeit kΓΌndbar.', 'en' => 'Auto-renewal every :period. Cancel anytime.'], + 'checkout.period.12months' => ['de' => '12 Monate', 'en' => '12 months'], + 'checkout.period.30days' => ['de' => '30 Tage', 'en' => '30 days'], + 'checkout.cancel_immediate' => ['de' => 'Abo wird sofort gekΓΌndigt', 'en' => 'Subscription will be cancelled immediately'], + 'checkout.cancel_desc' => ['de' => 'Du verlierst alle Premium-Funktionen. Stripe erstellt bei Bedarf automatisch eine Gutschrift fΓΌr nicht genutzte Tage.', 'en' => 'You will lose all premium features. Stripe will automatically issue a credit for unused days if applicable.'], + 'checkout.switch_immediate' => ['de' => 'Sofortige Umstellung', 'en' => 'Immediate switch'], + 'checkout.switch_desc' => ['de' => 'Stripe berechnet anteilig die Differenz fΓΌr die verbleibenden Tage. Keine neue Zahlung nΓΆtig.', 'en' => 'Stripe will prorate the difference for the remaining days. No new payment required.'], + 'checkout.cancel_btn' => ['de' => 'Abo kΓΌndigen & zu Free wechseln', 'en' => 'Cancel sub & switch to Free'], + 'checkout.cancelling' => ['de' => 'Wird gekΓΌndigt…', 'en' => 'Cancelling…'], + 'checkout.switch_btn' => ['de' => 'Jetzt zu :plan wechseln', 'en' => 'Switch to :plan now'], + 'checkout.switching' => ['de' => 'Wird gewechselt…', 'en' => 'Switching…'], + 'checkout.pay_now' => ['de' => 'Jetzt bezahlen', 'en' => 'Pay now'], + 'checkout.redirecting' => ['de' => 'Weiterleitung zu Stripe…', 'en' => 'Redirecting to Stripe…'], + 'checkout.redirect_stripe' => ['de' => 'Du wirst sicher zu Stripe weitergeleitet.', 'en' => 'You will be securely redirected to Stripe.'], + 'checkout.no_checkout' => ['de' => 'Kein Checkout nΓΆtig – Stripe verarbeitet die Γ„nderung direkt.', 'en' => 'No checkout needed – Stripe processes the change directly.'], + 'checkout.cancel_effective' => ['de' => 'KΓΌndigung wird sofort wirksam.', 'en' => 'Cancellation takes effect immediately.'], + + // ── Checkout Success ───────────────────── + 'checkout.success.processing' => ['de' => 'Zahlung wird verarbeitet…', 'en' => 'Processing payment…'], + 'checkout.success.wait' => ['de' => 'Bitte warte einen Moment. Du wirst automatisch weitergeleitet.', 'en' => 'Please wait a moment. You will be redirected automatically.'], + 'checkout.success.activate_sub' => ['de' => 'Abonnement aktivieren', 'en' => 'Activate subscription'], + 'checkout.success.sub_created' => ['de' => 'Abonnement erfolgreich angelegt', 'en' => 'Subscription successfully created'], + 'checkout.success.waiting_stripe' => ['de' => 'Warte auf BestΓ€tigung von Stripe…', 'en' => 'Waiting for Stripe confirmation…'], + 'checkout.success.done' => ['de' => 'Erledigt', 'en' => 'Done'], + 'checkout.success.capture' => ['de' => 'Zahlung erfassen', 'en' => 'Capture payment'], + 'checkout.success.payment_saved' => ['de' => 'Zahlung in Rechnungshistorie gespeichert', 'en' => 'Payment saved to invoice history'], + 'checkout.success.confirming' => ['de' => 'Zahlungseingang wird bestΓ€tigt…', 'en' => 'Confirming payment receipt…'], + 'checkout.success.unlock_plan' => ['de' => 'Plan freischalten', 'en' => 'Unlock plan'], + 'checkout.success.features_on' => ['de' => 'Alle Features werden aktiviert', 'en' => 'All features are being activated'], + 'checkout.success.all_ready' => ['de' => 'Alles bereit!', 'en' => 'All set!'], + 'checkout.success.sub_active' => ['de' => 'Dein Abonnement ist aktiv und die Zahlung wurde erfasst.', 'en' => 'Your subscription is active and the payment has been captured.'], + 'checkout.success.sub_activated' => ['de' => 'Abonnement aktiviert', 'en' => 'Subscription activated'], + 'checkout.success.plan_created' => ['de' => 'Dein Plan wurde erfolgreich angelegt', 'en' => 'Your plan was successfully created'], + 'checkout.success.payment_captured' => ['de' => 'Zahlung erfasst', 'en' => 'Payment captured'], + 'checkout.success.invoice_hint' => ['de' => 'Rechnung ist in deiner Rechnungshistorie', 'en' => 'Invoice is in your invoice history'], + 'checkout.success.plan_unlocked' => ['de' => 'Plan freigeschaltet', 'en' => 'Plan unlocked'], + 'checkout.success.all_features' => ['de' => 'Alle Features sind ab sofort verfΓΌgbar', 'en' => 'All features are now available'], + 'checkout.success.to_dashboard' => ['de' => 'Zum Dashboard', 'en' => 'Go to Dashboard'], + 'checkout.success.view_invoices' => ['de' => 'Rechnungen ansehen', 'en' => 'View invoices'], + + // ── Plans Page ────────────────────────── + 'plans.choose' => ['de' => 'WΓ€hle deinen Plan', 'en' => 'Choose your plan'], + 'plans.subtitle' => ['de' => 'Starte kostenlos. Upgrade jederzeit mΓΆglich.', 'en' => 'Start for free. Upgrade anytime.'], + 'plans.monthly' => ['de' => 'Monatlich', 'en' => 'Monthly'], + 'plans.yearly' => ['de' => 'JΓ€hrlich', 'en' => 'Yearly'], + 'plans.feature.all_in_one' => ['de' => 'Alles an einem Ort', 'en' => 'All in one place'], + 'plans.feature.all_in_one_desc' => ['de' => 'Termine, Notizen & Aufgaben zentral verwalten', 'en' => 'Manage events, notes & tasks in one place'], + 'plans.feature.smart_input' => ['de' => 'Smarte Eingabe', 'en' => 'Smart input'], + 'plans.feature.smart_input_desc' => ['de' => 'Einfach schreiben oder sprechen – wir verstehen dich', 'en' => 'Just type or speak – we understand you'], + 'plans.feature.ready' => ['de' => 'Sofort startklar', 'en' => 'Ready to go'], + 'plans.feature.ready_desc' => ['de' => 'Kein Setup – direkt loslegen', 'en' => 'No setup – start right away'], + 'plans.current_plan' => ['de' => 'Dein aktueller Plan:', 'en' => 'Your current plan:'], + 'plans.free' => ['de' => 'Kostenlos', 'en' => 'Free'], + 'plans.per_month' => ['de' => '/ Monat', 'en' => '/ Month'], + 'plans.per_year' => ['de' => '/ Jahr', 'en' => '/ Year'], + 'plans.devices' => ['de' => 'GerΓ€te', 'en' => 'Devices'], + 'plans.general' => ['de' => 'Allgemein', 'en' => 'General'], + 'plans.active_plan' => ['de' => 'Aktiver Plan', 'en' => 'Active plan'], + 'plans.start' => ['de' => 'Starten', 'en' => 'Get started'], + 'plans.why' => ['de' => 'Warum dieser Plan?', 'en' => 'Why this plan?'], + 'plans.cancel_anytime' => ['de' => 'Jederzeit kΓΌndbar', 'en' => 'Cancel anytime'], + 'plans.no_hidden_costs' => ['de' => 'Keine versteckten Kosten', 'en' => 'No hidden costs'], + 'plans.up_down_anytime' => ['de' => 'Upgrade & Downgrade jederzeit mΓΆglich', 'en' => 'Upgrade & downgrade anytime'], + 'plans.faq' => ['de' => 'Fragen & Antworten', 'en' => 'FAQ'], + 'plans.faq.switch' => ['de' => 'Kann ich jederzeit wechseln?', 'en' => 'Can I switch anytime?'], + 'plans.faq.switch_a' => ['de' => 'Ja, du kannst jederzeit upgraden oder downgraden.', 'en' => 'Yes, you can upgrade or downgrade at any time.'], + 'plans.faq.expire' => ['de' => 'Was passiert nach Ablauf?', 'en' => 'What happens when it expires?'], + 'plans.faq.expire_a' => ['de' => 'Dein Account wird automatisch auf den Free Plan zurΓΌckgesetzt.', 'en' => 'Your account will automatically revert to the Free plan.'], + + // ── Admin ─────────────────────────────── + 'admin.title' => ['de' => 'Admin Dashboard', 'en' => 'Admin Dashboard'], + 'admin.subtitle' => ['de' => 'Systemweite Statistiken und letzte AktivitΓ€ten', 'en' => 'System-wide statistics and recent activities'], + 'admin.total_users' => ['de' => 'Gesamt-User', 'en' => 'Total Users'], + 'admin.active_users' => ['de' => 'Aktive User', 'en' => 'Active Users'], + 'admin.new_this_month' => ['de' => 'Neu diesen Monat', 'en' => 'New this month'], + 'admin.pro_users' => ['de' => 'Pro-User', 'en' => 'Pro Users'], + 'admin.credits_month' => ['de' => 'Credits verbraucht (diesen Monat)', 'en' => 'Credits used (this month)'], + 'admin.costs_month' => ['de' => 'Kosten diesen Monat', 'en' => 'Costs this month'], + 'admin.last_activities' => ['de' => 'Letzte AktivitΓ€ten', 'en' => 'Recent Activities'], + 'admin.last_10' => ['de' => '10 neueste EintrΓ€ge', 'en' => '10 most recent entries'], + 'admin.no_logs' => ['de' => 'Keine Log-EintrΓ€ge vorhanden', 'en' => 'No log entries available'], + + // Admin: Users + 'admin.users.title' => ['de' => 'Benutzerverwaltung', 'en' => 'User Management'], + 'admin.users.total' => ['de' => 'Benutzer insgesamt', 'en' => 'Total users'], + 'admin.users.all' => ['de' => 'Alle', 'en' => 'All'], + 'admin.users.active' => ['de' => 'Aktiv', 'en' => 'Active'], + 'admin.users.suspended' => ['de' => 'Gesperrt', 'en' => 'Suspended'], + 'admin.users.blocked' => ['de' => 'Blockiert', 'en' => 'Blocked'], + 'admin.users.team' => ['de' => 'Team', 'en' => 'Team'], + 'admin.users.search' => ['de' => 'Name oder E-Mail suchen...', 'en' => 'Search name or email...'], + 'admin.users.user' => ['de' => 'Benutzer', 'en' => 'User'], + 'admin.users.plan' => ['de' => 'Plan', 'en' => 'Plan'], + 'admin.users.events' => ['de' => 'Events', 'en' => 'Events'], + 'admin.users.logs' => ['de' => 'Logs', 'en' => 'Logs'], + 'admin.users.registered' => ['de' => 'Registriert', 'en' => 'Registered'], + 'admin.users.last_login' => ['de' => 'Letzter Login', 'en' => 'Last Login'], + 'admin.users.calendar' => ['de' => 'Kalender', 'en' => 'Calendar'], + 'admin.users.change_status' => ['de' => 'Status Γ€ndern', 'en' => 'Change status'], + 'admin.users.not_found' => ['de' => 'Keine Benutzer gefunden.', 'en' => 'No users found.'], + 'admin.users.of_total' => ['de' => 'von :total Benutzern', 'en' => 'of :total users'], + + // Admin: User Detail + 'admin.detail.back' => ['de' => 'ZurΓΌck zur Übersicht', 'en' => 'Back to overview'], + 'admin.detail.free' => ['de' => 'Free', 'en' => 'Free'], + 'admin.detail.events' => ['de' => 'Events', 'en' => 'Events'], + 'admin.detail.notes' => ['de' => 'Notizen', 'en' => 'Notes'], + 'admin.detail.tasks' => ['de' => 'Aufgaben', 'en' => 'Tasks'], + 'admin.detail.contacts' => ['de' => 'Kontakte', 'en' => 'Contacts'], + 'admin.detail.credits_month' => ['de' => 'Credits (Monat)', 'en' => 'Credits (Month)'], + 'admin.detail.total_costs' => ['de' => 'Gesamtkosten', 'en' => 'Total costs'], + 'admin.detail.last_activity' => ['de' => 'Letzte AktivitΓ€t', 'en' => 'Last activity'], + 'admin.detail.never' => ['de' => 'Nie', 'en' => 'Never'], + 'admin.detail.credit_balance' => ['de' => 'Credit-Guthaben', 'en' => 'Credit balance'], + 'admin.detail.change_role' => ['de' => 'Rolle Γ€ndern', 'en' => 'Change role'], + 'admin.detail.current_role' => ['de' => 'Aktuelle Rolle:', 'en' => 'Current role:'], + 'admin.detail.save_role' => ['de' => 'Rolle speichern', 'en' => 'Save role'], + 'admin.detail.internal' => ['de' => 'Interner Account', 'en' => 'Internal Account'], + 'admin.detail.internal_desc' => ['de' => 'kein Credit-Limit Β· interner Plan Β· kein Affiliate-Programm.', 'en' => 'no credit limit Β· internal plan Β· no affiliate program.'], + 'admin.detail.transactions' => ['de' => 'Credit-Transaktionen', 'en' => 'Credit Transactions'], + 'admin.detail.no_transactions' => ['de' => 'Keine Transaktionen vorhanden.', 'en' => 'No transactions available.'], + 'admin.detail.by' => ['de' => 'von', 'en' => 'by'], + 'admin.detail.current_access' => ['de' => 'Aktueller Zugang', 'en' => 'Current Access'], + 'admin.detail.expires' => ['de' => 'LΓ€uft ab:', 'en' => 'Expires:'], + 'admin.detail.unlimited' => ['de' => 'Unbegrenzt gΓΌltig', 'en' => 'Unlimited validity'], + 'admin.detail.gifted_by' => ['de' => 'Geschenkt von', 'en' => 'Gifted by'], + 'admin.detail.admin' => ['de' => 'Admin', 'en' => 'Admin'], + 'admin.detail.confirm_revoke' => ['de' => 'Zugang wirklich entziehen? Der Benutzer wird auf den Free-Plan zurΓΌckgesetzt.', 'en' => 'Really revoke access? The user will be reset to the Free plan.'], + 'admin.detail.revoke' => ['de' => 'Zugang entziehen', 'en' => 'Revoke access'], + 'admin.detail.no_access' => ['de' => 'Kein aktiver Zugang vorhanden.', 'en' => 'No active access available.'], + 'admin.detail.gift_access' => ['de' => 'Zugang schenken', 'en' => 'Gift access'], + 'admin.detail.choose_plan' => ['de' => 'Plan wΓ€hlen...', 'en' => 'Choose plan...'], + 'admin.detail.duration' => ['de' => 'Dauer', 'en' => 'Duration'], + 'admin.detail.1month' => ['de' => '1 Monat', 'en' => '1 month'], + 'admin.detail.3months' => ['de' => '3 Monate', 'en' => '3 months'], + 'admin.detail.6months' => ['de' => '6 Monate', 'en' => '6 months'], + 'admin.detail.1year' => ['de' => '1 Jahr', 'en' => '1 year'], + 'admin.detail.unlimited_dur' => ['de' => 'Unbegrenzt', 'en' => 'Unlimited'], + 'admin.detail.reason' => ['de' => 'Grund (optional)', 'en' => 'Reason (optional)'], + 'admin.detail.reason_ph' => ['de' => 'z.B. Beta-Tester, Partnerschaft...', 'en' => 'e.g. Beta tester, partnership...'], + 'admin.detail.gift_credits' => ['de' => 'Credits schenken', 'en' => 'Gift credits'], + 'admin.detail.credit_amount' => ['de' => 'Anzahl Credits', 'en' => 'Credit amount'], + 'admin.detail.credit_reason' => ['de' => 'Grund der Gutschrift', 'en' => 'Reason for credit'], + 'admin.detail.give_credits' => ['de' => 'Credits vergeben', 'en' => 'Give credits'], + 'admin.detail.agent_logs' => ['de' => 'Agent-Logs', 'en' => 'Agent Logs'], + 'admin.detail.entries' => ['de' => 'EintrΓ€ge', 'en' => 'Entries'], + 'admin.detail.confirm_refund' => ['de' => 'Credits zurΓΌckerstatten und Log lΓΆschen?', 'en' => 'Refund credits and delete log?'], + 'admin.detail.delete_refund' => ['de' => 'LΓΆschen & Refund', 'en' => 'Delete & Refund'], + 'admin.detail.of_entries' => ['de' => 'von :total EintrΓ€gen', 'en' => 'of :total entries'], + + // Admin: User Calendar + 'admin.calendar.back_to' => ['de' => 'ZurΓΌck zu', 'en' => 'Back to'], + 'admin.calendar.title' => ['de' => 'Kalender von', 'en' => 'Calendar of'], + 'admin.calendar.events_total' => ['de' => 'Events insgesamt', 'en' => 'Total events'], + 'admin.calendar.no_events' => ['de' => 'Keine Events vorhanden.', 'en' => 'No events available.'], + 'admin.calendar.no_date' => ['de' => 'Ohne Datum', 'en' => 'No date'], + 'admin.calendar.all_day' => ['de' => 'GanztΓ€gig', 'en' => 'All day'], + 'admin.calendar.event_details' => ['de' => 'Event-Details', 'en' => 'Event Details'], + 'admin.calendar.read_only' => ['de' => 'Nur-Lesen Ansicht', 'en' => 'Read-only view'], + 'admin.calendar.notes' => ['de' => 'Notizen', 'en' => 'Notes'], + 'admin.calendar.details' => ['de' => 'Details', 'en' => 'Details'], + 'admin.calendar.created' => ['de' => 'Erstellt', 'en' => 'Created'], + 'admin.calendar.updated' => ['de' => 'Aktualisiert', 'en' => 'Updated'], + 'admin.calendar.google_sync' => ['de' => 'Google Sync', 'en' => 'Google Sync'], + 'admin.calendar.connected' => ['de' => 'Verbunden', 'en' => 'Connected'], + + // Admin: Plans + 'admin.plans.title' => ['de' => 'PlΓ€ne', 'en' => 'Plans'], + 'admin.plans.subtitle' => ['de' => 'Verwalte ΓΆffentliche und interne PlΓ€ne', 'en' => 'Manage public and internal plans'], + 'admin.plans.new' => ['de' => 'Neuer Plan', 'en' => 'New Plan'], + 'admin.plans.public' => ['de' => 'Γ–ffentliche PlΓ€ne', 'en' => 'Public Plans'], + 'admin.plans.plans' => ['de' => 'PlΓ€ne', 'en' => 'Plans'], + 'admin.plans.popular' => ['de' => 'Beliebt', 'en' => 'Popular'], + 'admin.plans.confirm_delete' => ['de' => 'Plan \':name\' wirklich lΓΆschen?', 'en' => 'Really delete plan \':name\'?'], + 'admin.plans.free' => ['de' => 'Kostenlos', 'en' => 'Free'], + 'admin.plans.per_month' => ['de' => '/ Monat', 'en' => '/ Month'], + 'admin.plans.devices' => ['de' => 'GerΓ€te', 'en' => 'Devices'], + 'admin.plans.no_public' => ['de' => 'Keine ΓΆffentlichen PlΓ€ne vorhanden.', 'en' => 'No public plans available.'], + 'admin.plans.internal' => ['de' => 'Interne PlΓ€ne', 'en' => 'Internal Plans'], + 'admin.plans.not_public' => ['de' => 'Nicht ΓΆffentlich', 'en' => 'Not public'], + 'admin.plans.role.admin' => ['de' => 'Admin / Super Admin', 'en' => 'Admin / Super Admin'], + 'admin.plans.role.developer' => ['de' => 'Developer', 'en' => 'Developer'], + 'admin.plans.role.support' => ['de' => 'Support', 'en' => 'Support'], + 'admin.plans.role.affiliate' => ['de' => 'Affiliate', 'en' => 'Affiliate'], + 'admin.plans.role.beta' => ['de' => 'Beta Tester', 'en' => 'Beta Tester'], + 'admin.plans.internal_badge' => ['de' => 'intern', 'en' => 'internal'], + 'admin.plans.auto_assigned' => ['de' => 'Automatisch zugewiesen', 'en' => 'Automatically assigned'], + 'admin.plans.no_plans' => ['de' => 'Noch keine PlΓ€ne angelegt', 'en' => 'No plans created yet'], + + // Admin: Plan Form + 'admin.plans.form.edit' => ['de' => 'Plan bearbeiten', 'en' => 'Edit Plan'], + 'admin.plans.form.new' => ['de' => 'Neuer Plan', 'en' => 'New Plan'], + 'admin.plans.form.step' => ['de' => 'Schritt :step von 2', 'en' => 'Step :step of 2'], + 'admin.plans.form.general' => ['de' => 'Allgemein', 'en' => 'General'], + 'admin.plans.form.features' => ['de' => 'Features', 'en' => 'Features'], + 'admin.plans.form.name' => ['de' => 'Name', 'en' => 'Name'], + 'admin.plans.form.name_ph' => ['de' => 'Plan-Name', 'en' => 'Plan name'], + 'admin.plans.form.price' => ['de' => 'Preis (Cent)', 'en' => 'Price (cents)'], + 'admin.plans.form.discount' => ['de' => 'Jahresrabatt (Monate)', 'en' => 'Yearly discount (months)'], + 'admin.plans.form.credits' => ['de' => 'Credits / Monat', 'en' => 'Credits / Month'], + 'admin.plans.form.devices' => ['de' => 'GerΓ€te-Limit', 'en' => 'Device limit'], + 'admin.plans.form.ai_config' => ['de' => 'AI-Konfiguration', 'en' => 'AI Configuration'], + 'admin.plans.form.model' => ['de' => 'Modell', 'en' => 'Model'], + 'admin.plans.form.model_ph' => ['de' => 'Modell auswΓ€hlen', 'en' => 'Select model'], + 'admin.plans.form.max_tokens' => ['de' => 'Max Tokens', 'en' => 'Max Tokens'], + 'admin.plans.form.temperature' => ['de' => 'Temperature', 'en' => 'Temperature'], + 'admin.plans.form.active' => ['de' => 'Plan aktiv', 'en' => 'Plan active'], + 'admin.plans.form.active_desc' => ['de' => 'FΓΌr Nutzer sichtbar und buchbar', 'en' => 'Visible and available to users'], + 'admin.plans.form.featured' => ['de' => 'Empfohlen (Featured)', 'en' => 'Recommended (Featured)'], + 'admin.plans.form.featured_desc' => ['de' => 'Wird hervorgehoben und mit "Beliebt" Badge angezeigt', 'en' => 'Highlighted with a "Popular" badge'], + 'admin.plans.form.assign' => ['de' => 'Klicke Features an um sie diesem Plan zuzuweisen', 'en' => 'Click features to assign them to this plan'], + 'admin.plans.form.feature' => ['de' => 'Feature', 'en' => 'Feature'], + 'admin.plans.form.back' => ['de' => '← ZurΓΌck', 'en' => '← Back'], + 'admin.plans.form.next' => ['de' => 'Weiter β†’', 'en' => 'Next β†’'], + + // Admin: Affiliates + 'admin.affiliates.title' => ['de' => 'Affiliate-Programm', 'en' => 'Affiliate Program'], + 'admin.affiliates.subtitle' => ['de' => 'Übersicht aller Affiliates, Referrals und Gutschriften', 'en' => 'Overview of all affiliates, referrals and credits'], + 'admin.affiliates.affiliates' => ['de' => 'Affiliates', 'en' => 'Affiliates'], + 'admin.affiliates.active' => ['de' => 'Aktiv', 'en' => 'Active'], + 'admin.affiliates.referrals' => ['de' => 'Referrals', 'en' => 'Referrals'], + 'admin.affiliates.pending' => ['de' => 'Ausstehend', 'en' => 'Pending'], + 'admin.affiliates.paid' => ['de' => 'Bezahlt', 'en' => 'Paid'], + 'admin.affiliates.credits_earned' => ['de' => 'Credits verdient', 'en' => 'Credits earned'], + 'admin.affiliates.search' => ['de' => 'Name, E-Mail oder Code suchen...', 'en' => 'Search name, email or code...'], + 'admin.affiliates.affiliate' => ['de' => 'Affiliate', 'en' => 'Affiliate'], + 'admin.affiliates.code' => ['de' => 'Code', 'en' => 'Code'], + 'admin.affiliates.paused' => ['de' => 'Pausiert', 'en' => 'Paused'], + 'admin.affiliates.blocked' => ['de' => 'Gesperrt', 'en' => 'Blocked'], + 'admin.affiliates.pause' => ['de' => 'Pausieren', 'en' => 'Pause'], + 'admin.affiliates.activate' => ['de' => 'Aktivieren', 'en' => 'Activate'], + 'admin.affiliates.confirm_block' => ['de' => 'Affiliate wirklich sperren?', 'en' => 'Really block affiliate?'], + 'admin.affiliates.block' => ['de' => 'Sperren', 'en' => 'Block'], + 'admin.affiliates.unblock' => ['de' => 'Entsperren', 'en' => 'Unblock'], + 'admin.affiliates.none' => ['de' => 'Keine Affiliates vorhanden.', 'en' => 'No affiliates available.'], + 'admin.affiliates.of_total' => ['de' => 'von :total Affiliates', 'en' => 'of :total affiliates'], + + // Admin: Features Form + 'admin.features.edit' => ['de' => 'Feature bearbeiten', 'en' => 'Edit Feature'], + 'admin.features.new' => ['de' => 'Neues Feature', 'en' => 'New Feature'], + 'admin.features.configure' => ['de' => 'Feature konfigurieren', 'en' => 'Configure Feature'], + 'admin.features.name' => ['de' => 'Name', 'en' => 'Name'], + 'admin.features.name_ph' => ['de' => 'Kalender & Termine', 'en' => 'Calendar & Events'], + 'admin.features.key' => ['de' => 'Key', 'en' => 'Key'], + 'admin.features.icon' => ['de' => 'Icon auswΓ€hlen', 'en' => 'Select icon'], + 'admin.features.category' => ['de' => 'Kategorie', 'en' => 'Category'], + 'admin.features.none' => ['de' => 'Keine', 'en' => 'None'], + ]; + + foreach ($translations as $key => $values) { + foreach ($values as $locale => $value) { + Translation::updateOrCreate( + ['key' => $key, 'locale' => $locale], + ['value' => $value] + ); + } + } + + cache()->forget('translations:de'); + cache()->forget('translations:en'); + + $count = count($translations); + $this->command->info("{$count} Keys Γ— 2 Sprachen = " . ($count * 2) . " Übersetzungen gespeichert."); + } +} diff --git a/src/package-lock.json b/src/package-lock.json new file mode 100644 index 0000000..5e04572 --- /dev/null +++ b/src/package-lock.json @@ -0,0 +1,2084 @@ +{ + "name": "src", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "interactjs": "^1.10.27", + "laravel-echo": "^2.3.4", + "pusher-js": "^8.5.0", + "three": "^0.183.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": ">=1.11.0 <=1.14.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^3.0.0", + "tailwindcss": "^4.0.0", + "vite": "^8.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@interactjs/types": { + "version": "1.10.27", + "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.27.tgz", + "integrity": "sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT", + "peer": true + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interactjs": { + "version": "1.10.27", + "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.27.tgz", + "integrity": "sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==", + "license": "MIT", + "dependencies": { + "@interactjs/types": "1.10.27" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-echo": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.4.tgz", + "integrity": "sha512-rpALCIK1uw2SrttcK9P5JzItt5I85RcfXQKUNnkcorzhtKeXi5GS0PVFFBH8ppNo8wnbdBKuD1EtIHgTbXo9FQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "pusher-js": "*", + "socket.io-client": "*" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.0.1.tgz", + "integrity": "sha512-Bx8sVcLIaZT1d0eisABcmjQ1GZdJpaXcV66A8RhXGp9JgR3iL8jDnvakVDXuH87Tn5S9KNx3VOhmJZW1CSexOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "tinyglobby": "^0.2.12", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^8.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pusher-js": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.5.0.tgz", + "integrity": "sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw==", + "license": "MIT", + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/three": { + "version": "0.183.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", + "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000..870174b --- /dev/null +++ b/src/package.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --mode development", + "build": "vite build --mode development", + "build:prod": "vite build --mode production", + "build:staging": "vite build --mode staging" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": ">=1.11.0 <=1.14.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^3.0.0", + "tailwindcss": "^4.0.0", + "vite": "^8.0.0" + }, + "dependencies": { + "interactjs": "^1.10.27", + "laravel-echo": "^2.3.4", + "pusher-js": "^8.5.0", + "three": "^0.183.2" + } +} diff --git a/src/phpunit.xml b/src/phpunit.xml new file mode 100644 index 0000000..e7f0a48 --- /dev/null +++ b/src/phpunit.xml @@ -0,0 +1,36 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + diff --git a/src/public/.htaccess b/src/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/src/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/src/public/favicon.ico b/src/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/src/public/images/logo-text.png b/src/public/images/logo-text.png new file mode 100644 index 0000000000000000000000000000000000000000..e7d42bf2088a688e26a14fb492ccfe464ba48aac GIT binary patch literal 9455 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

M*J_@u_0 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 F{|{>WuEGES literal 0 HcmV?d00001 diff --git a/src/public/images/logo.png b/src/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..978790556058a4f707ddb46864f18b9556a9eb11 GIT binary patch literal 14393 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 literal 0 HcmV?d00001 diff --git a/src/public/index.php b/src/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/src/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/src/public/robots.txt b/src/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/src/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/src/resources/css/app.css b/src/resources/css/app.css new file mode 100644 index 0000000..f79827b --- /dev/null +++ b/src/resources/css/app.css @@ -0,0 +1,123 @@ +@import 'tailwindcss'; + +@import "../plugins/Notification/src/message.css"; +@import "../fonts/BaiJamjuree/font.css"; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../**/*.blade.php'; +@source '../**/*.js'; + + +@theme { + /* Fonts */ + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-bai: 'Bai Jamjuree', sans-serif; + /*--font-space: 'Space Age', sans-serif;*/ + /*--font-syne: 'Syne', sans-serif;*/ + + /* ======================== + PRIMARY (clean modern blue) + ======================== */ + --color-primary: #6366f1; + + --color-primary-50: #EEF2FF; + --color-primary-100: #E0E7FF; + --color-primary-200: #C7D2FE; + --color-primary-300: #A5B4FC; + --color-primary-400: #818CF8; + --color-primary-500: #4F46E5; + --color-primary-600: #4338CA; + --color-primary-700: #3730A3; + --color-primary-800: #312E81; + --color-primary-900: #1E1B4B; + + /* ======================== + BACKGROUND + ======================== */ + --color-background: #F6F7FB; + --color-surface: #FFFFFF; + + /* ======================== + TEXT + ======================== */ + --color-text: #0F172A; + --color-text-muted: #64748B; + --color-text-light: #94A3B8; + + /* ======================== + BORDER + ======================== */ + --color-border: #E2E8F0; + --color-border-strong: #CBD5E1; + + /* ======================== + SIDEBAR + ======================== */ + --color-sidebar: #FFFFFF; + --color-sidebar-hover: #F1F5F9; + --color-sidebar-active: #4F46E5; + + /* ======================== + STATUS + ======================== */ + --color-success: #22C55E; + --color-danger: #EF4444; + --color-warning: #F59E0B; + + /* ======================== + SHADOW + ======================== */ + --shadow-card: 0 8px 24px rgba(15, 23, 42, 0.05); + --shadow-hover: 0 12px 30px rgba(15, 23, 42, 0.08); + + /* ======================== + RADIUS + ======================== */ + --radius-card: 14px; + --radius-button: 10px; +} + +/* Chrome, Safari */ +::-webkit-scrollbar { + width: 0px; + height: 0px; +} + +/* Firefox */ +* { + scrollbar-width: none; +} + +html { + @apply font-bai +} + +.calendar-allday-event { + will-change: left; +} + + +.allday-drop-preview { + background: rgba(59, 130, 246, 0.15); +} + +.allday-drop-line { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + background: rgb(59, 130, 246); + pointer-events: none; + z-index: 50; +} + +.calendar-event { + transition: transform 0.2s ease; +} + +.input { + @apply w-full mt-1 border border-border rounded-lg px-3 py-2 text-sm; +} + diff --git a/src/resources/css/safelist-tailwindcss.txt b/src/resources/css/safelist-tailwindcss.txt new file mode 100644 index 0000000..c99c89b --- /dev/null +++ b/src/resources/css/safelist-tailwindcss.txt @@ -0,0 +1,5 @@ +sm:max-w-md +md:max-w-xl +lg:max-w-3xl +xl:max-w-5xl +2xl:max-w-7xl diff --git a/src/resources/fonts/BaiJamjuree/bai-jamjuree-200.woff2 b/src/resources/fonts/BaiJamjuree/bai-jamjuree-200.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..25553f958caadb920c7a0dd06f5acfcff9e9fce6 GIT binary patch literal 10428 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ν=9tfXyz@4%YDc3EAerIctOyOgY6->hXDRr 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 y=$#ihnrK!CMXTeziP*<%<>uB_2O$KcM literal 0 HcmV?d00001 diff --git a/src/resources/fonts/BaiJamjuree/bai-jamjuree-300.woff2 b/src/resources/fonts/BaiJamjuree/bai-jamjuree-300.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d3db0599ee91d8f188dd9870cae07a21b770a42a GIT binary patch literal 10728 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 aThX(-UQ*`gLga547>ef>wW7oU0000uVmw^{ literal 0 HcmV?d00001 diff --git a/src/resources/fonts/BaiJamjuree/bai-jamjuree-500.woff2 b/src/resources/fonts/BaiJamjuree/bai-jamjuree-500.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..dfc55e1e5f6d10f99cff50f4e02f17725b74e327 GIT binary patch literal 10808 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

AjPDjUhZ$%0IE|NB893h&2(^N4*I?a>Cps5 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 Sqy${|ac#7_uYGOJBn1F~jEd6$ literal 0 HcmV?d00001 diff --git a/src/resources/fonts/BaiJamjuree/bai-jamjuree-600.woff2 b/src/resources/fonts/BaiJamjuree/bai-jamjuree-600.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0c1295a39089ae6538d36d093a8501fb8e22d60c GIT binary patch literal 10840 zcmV-eDyP+VPew8T0RR9104i7j4gdfE0ArK@04e|g0RR9100000000000000000000 z0000QSR1W29D`B@U;u>(5eN$Q4BjRSgLVJ`HUcCAhiC*K1%wO-hF}}RR5Js?#sLrk zcYa3^Y#b2$Hp3`NCuh>&|B%298CVVVPm~Y@MDdPAkl5_l8N~_8@xDWW&Kpzlnf-6H z)(S;OOB+C9?Zh*5d!7>z299|AZ22Djtn6| z6_e{0C%GWn-psbWFdb~(lmk~wSE|DZgpN@Z55i&`me-W}|A{(%gr25KzhsWK;%H0e z(snXO+cdvVXZ~5qff>na+7MI?EeI~R_K_R_a4WrZK-2*iRVrYx+p7GuI>M~-~aNg{pTaP!J}w=p>YVwI~k(hHCN{yF5-h&6Y*#FKTIO2|A=YR zq@fg4mRe;f?pVesL>c!wbG~2RC^YFKOJjsFt}sCuVT5afIr5eEy^O62FZD~YV8~7o zN0|RFP=FZ#6h2ZQBuEg%DjkxkEu_5)NR=u`SKT2!4S)g4!4dHZ^DpT$I{-m|`J~ybf=+L$Whqi0pPaXlVkaumq6rN!)W@^yv+smcSt9 z#G0%~DQ}d(QRIFH`6jZ;Niy~_P)mV@<_jB%k!nx*jdLkh8t$)x-%(8F=Y>F>dMm84 z(N4SUw#PpEH8=qCLZZp>Q7a)Jgoun}l7fVai1C05!(&2k7HINf=~&K_7W@*BC#v zz!L1)y8zBfSd zEQkv9wb6wfE_U3Eb*vODoHCjqPLuGMeLPk5++iS9H z#9e$C0yDC2YH!XAY-2%5B8^8)04FD<`Zn~{(ix{Li2xEPZ+hWE5^Mb=r?GClcU{pW z))^P8>na#!{JbS=6SVk1K33(t(rY;cKTm#e5U| zH3l|!FtmW(U5U^^03ier%r1lgLI@!`6$#aX_+kVP7!(MQkU$PO_>e;e1LQD34g(Zm zLkTT7prH(0LkqnPraS8&+o_rh zRn?{%&4qd~l8}-Ekrc9|tDtHi2m>V-B;`@09mU!!HUoqq4^&{Ogqu`ZjTNCzSuz9% zT<}0a0vb|K%2%#z&Fm$13zO53V9K7!MM?o(n#1Zk@lIt&=*mb~r}hKHn5|s}(ebM; zeJ>iQ+n3l(oHyvzLH3t?E2(u32D;T|O4k?bt^K<|bfEnVobq@P71p&#!2#sS6K68dPBA`VPi$&)V@|FTV=qp$2+=c+6p8Kt zSYr(KjV170)<=3%KQT|yjx-#nF}^^`ExUHn4ZJNm?}qs`vX=yX*JNMJvZegJB(Y_?VtMFx}p?-G1{i)dN@& zuio$HF?ImWXx186Y8iL}rYwEnIg&X-3R|$zQlBm${VDu&3$SKRnZjGCtfi)sb zZU1PfMu&88-F|!Gn#4j9aW2)rH1Z(hEr^O$GzJ!dM3dp;Lj;5{5fMZ}0Z~!GjDefA zfPjHPfCLL7u%QA6a@EWr>brmk9>mH6{9uMB{J~ZIBnBSz;6V=q_z*w}A+!(#lVO1p zBj{r!J&dNev1E*+mpUrdQ(*;_HmZ91f({B$ph5LVL-<07A}!K*0LMXh+CI z>^E)iUL~J7I-;!?=DRzL885;Cue0C z%IS4$Cb{|4yDRA1#hcwM=GqKtrUovyD?(ORTv4ODI3ns_3nnYkQGskR!URA#h8k{^ z(Z(2S9Bf#-#xzE@wIkKzni|;&AK0|7`=J-Rp+F%)AyL}f>#^;vK*F5NFTw~MO6aH~ z@)@9BaWy;GxUt;!^yR^-ynj~g5s<`|>e}kcVu(59DD4g{ja2~fi3rK9NFap_!Sz#( zFga@T9ht6Ot%qYdJI3fB654&07$4i9$vYSb%R9xNoj-dQmJBic=!y5qmm+L5mdquw zsWBn8#hX~j#mLOS{JD=nGSiZxEXtM_nDZd!axvUKJ`7~i`Y2o?HaOvO|H3)Ee?D>!NIxl~A)z<0dp*Fc^a9CdUi1BbxMg~Zpr0ARw$ z#aoW5W&-j{H2@U?c<%)WNFmGh(mN1?qy=WOYGsHW{-5T>Qw?>Z-m4Fm%NZ?WWXy~Y z6Usy|NsNox(3(A_w=+AbS@Hh_U?zr5ggU#<=;p5dy=9D=(Qi=*)9N*b`nSMo0D1-R z|5iqZRsitr9gl`Q+V*JtqufWPN5+ST9;QD`d(ik`4F9}h z@#n)=AU~G`Xdz6v=As1aAwjG-N#Z3+mZps?|7oo)y8?wWbWx^Qi3+7IE9cZpZ#DX< z)}KYC*Iqf`pnV!N+7Ch9L;!yS@+Pp`$)T3+7d>rS=yJ zkXSwwYqI@GSK?{0tj?)S>p{FO z@F$guU#2(rVUcZfHT_{L#nG*jRY`0a%r5Z>@%~1;IsK95xl*5YqJ|{rl@K8MJ-V_G zv2b$|3=nB;p+;PHV3^~Fu)NTgii+As)U7B+HnH|qT2zF)mo5z*ZZ4KxYw`9tCQMmzrWc2w5_AS>A~gX*#S^<;aJOtw`vK~I9YsHeB#2G(7zh|r zP+TPAqrbre4!>gDC^HcQdvjh?97xDPq?icBP3cVLLgllJa}BB?s+562#kPbht0XJL z)9DUGG;%3?aK)kWkPfI$8=0offR)!fq^oDIWZe*)^2skB4$DkHp5t@7gGPz zb_?mAJqB(@&M6nU7$<4QdCKL`1hp`rcxpF3CFlsyyo&@`BJ|d^x>A7RPu4*(ov>oK zHqojKls!~hu%D^xFYC*b0X{a(Mk|D-RednKLtlCWPiji#L%T0~hV>8SbUs61LHiD8 z@IOPqREcTNWCIEVextnYfZPzzgO8{F7!)$io{^rkWZp}B&b@@dkxzDG636bsu;a%% z(HSY28f&1aW}ZGut1t`ia16C=eQ;#}(BOcnM(CX?R)PETPh z81E>(qMlp(b-S(2gkc6XO;ibK;yr&wX@x=E)=pcM6P?w)y$MAZ`z;$NFc_zzsV_KR z(^8i~H(t;pnv}R3K-vW7**Ne%QYTzlsKQ(KJzd7q`EOQ>Iqh>xhp)Ary&@-+SUAfH z1*ti<`G69Enf+%Eh%kc-0bS|yY*_%7OJtq&_~%|-tZr0h^}7cdl*blwep8him>r4Y zGyo>(BQT$W6H+h-4V2-ixBWQACNIURtcZO!vKOhkP`SZ?^G!?^5hUe*o=LSqErqNM zY(isqqY0Ryg(stY+4`(Kok<}nFHOgoFsiCaFiY2{CfUv=l5=XqR+F>snL>jF6Fys* zTFIvv0|G<|2aMKBa#yer#w*f+FiN}K9~)=bE*g$i3rI|oHanECu0wMq0DNFiZJq%Q z6c9(4q!*uBUXzoM-Fl}@;qhY_i4lP&oN1}o5X+12cS1-Zp%&d5v?bY(3gDaQ=T{n{ zQj~zfF5vX;H8ZE8VC~AoS$M6kocq>G_lA?wGSJ;d+geP*gmgm-ppGpQU9rR9mPj8~ zp}So`k`kHhyl93{n#QG1EQn@FOARJG0YIY;RcyMIpRNGO2(qkK?~yN{Yv?r@Wq<+> zn8xw;kKfq&6c{2Wu%<$+3jh#QFoVyS?s;Kdjoz{Z$>`*?FWN_$;e;1}So}^khD<5L z$EDVPH+gQ%`CdY)mtp!*yJ?Y5&~jx*4>%nY+eH-)xJL|zf0&Zi%4I0zpG z1!grs{A7QtAtolQhi;7Mcjp96!2JBE7on&-WhOXzO_PTzk#9%)(rOC}Ee#Iz`-^?nk zCmK&&Fx`vp509M$SJUBKv%qDg?r-Q##(A-WJH{FpquC=ruBZzYw)dBl&7O1xK!^K^ zRfO8waHL_kCYt12D;{d1hyk$=-`_`rDCNbI#1=HwTh+Sm1KnYonZ<=ebYUmMxyYpO4)-xprEb z`9?=SLj$h=D0ypSG+^PAI$y7=#T!qY{l4D-{gIlh%Iti&XLk7Iotc|J;`Hwo>)(iE zBb^oS5oQ#dR9^6~c}ONN8c~J8EFD(%1^O;8C38SgmRb5?;^SH2R*Q0UL}e)h-E@T> zq>s;x_Xc|_z&>4>seoHB359UjjKX4DuZt~m>7T8pib=c9OJimm<2IB?PROOr(GX<7 ziNCvYxSKvCh&Nc&w zo*+$-$!B3luhki+Or<>QxcB;1o-%lJ(2L=)!wgHoR(@GwGsG=;u)kMlx6M_29E+N5 zp@D}e_l}-DB&(6P-y#B1h}ca6m7_dow)m0w?kVBTFUH7sFRD0(*$J8fe}pvq6)eY0 zPUcaG3>J*=9wf0k4rK4?@-7W}G-AHUo?>XnR8fc@QI^?e#u#TSiHsZf-P7%>3A@x9 zzSxmrYEf_n`6$E7eB`fyFEJ&BY}oQjn^z#VAtRcYaY^=zQ$f^wd3Hv<=ut!V#&&GG~Ea$VZt_;td3iqD%}5`s=I>s z5>~g38ZsV_*P97ieI@l86P2XW577R3QoZz5qPol8iffB>gQ=L9U46pJY%W#DCnq5T zyCM5@(ai*TRme_Y^aY1X1Bsz7W&^`OUw|LA4Tr*dUVo3f>P|KB5audfp`k(`jgo1#bUSSUe#|x!3fZoMYFNMa z^wVkVYB0P9Ng<^Ecp#um$|*!Daan5FUKHH)e9ALVKSr~61gOt)rKXoUjYzTRFDZDT zAS722HF$<#@M@$u6bfiKOsP)jmmyRGLjgtDUsFNsG+H&NCE{UwIN`FOMzXC7?s<1N z8wGjBRy=+yk$5z%`zUb>dKCqR*(T-YKfamW;dGby3i|C}IF%8CIoDr3cHB9Go_X`F zv6tR_YygvOMmr59buPnkQzEeMuY!ovfjSA7+PH*|a&Xy3e5b9KuxPj!y;@S^4|PeY z_s(%&B1u8)Y)RuX^^jHi^-}e`w`+^A#K7L@`{@=<^xMvmuXH~E4y69rqO&fOr*=W z84~tOJxp@DmWi+N-`S{MuHIzJHqinbEz96^@VOoxdZA8VbQY9j zzeCky5lye^qtX;G@@WPCKs}e;SZArh*@lNO^SbW=P@13B&d=mzss5M8OJ&} zolcSui>*l!@9ZvWlnjb0WA+VzAxs3Z6}#bXPN%A^r9D0MLlw1&`CWjOWZQ(WQt%yG zL6QhoEsYlh2Aew06$WI+ZXdID>FQr{lo~;cDdzETzyZ6}JsU`cj*K2zU%LKB!YQ0B zZtZE1@*d9=h-D=`Dk#5TXw%`;&V= zJfR0TlPQtcX0-o1-ARFys@E84`bCt2+lB*91QxQ|Sz4ilMc>D$Pd9QJVoPH@qfDld zo7BRef<0HS$1*tPmcC5QW(gMG7^`Wli8X>Dh{LM#tDIJiQsd+_uadZ0UhQyoJk}y? zd90Pay#-vyWMz^;(~qVbc}}7$YBMr1R}l}UYvr;$nME@ynzN zaX15MX}CIf(&Lgn7__9A(a@<59g-+bYM%YtCI%O3*CKdcdBQo@m z6p`p529frO0Gz=@xqL*1(s@jTds-MhZt0@xGzq>is-mgov68@K)aM0~xEiMwCDTG3 zB2Mu7Br{^pdwios? z9ITvMv9@2XdBN5L;`#TtHwsM}nM`Id$U?2#o9IoOvGtADHmlA0I#FYNt+tNU7;Fv6 z3!AjuvAaIgc;S-{ z`y)oP!lL4nL?(?~0y?f#B!f>+J4mw3=ShA`1PmVSlR>TRAH0a3<+gJS5;-I{NH}fH z$jYh@-q*E4eKM5_`U{(?Z>vc(FdO@8+v-w{>|2|!sc&k0yN%KIc4JfhHO=70YmQal zjsQ>fBmtiX@r0H@{>HFQi7Hoz^-qDkfhM)p!SUA0Lx7U};2Un~eM$M#<3EqLz6qV& z^j8(_o0h)$^U9wT`23^5f9^G(^mmg<9dx9($Qf4&BMCl?s3I(Wzlh9u?f_-_tbbdj z?HNEw)woB7>}#lt2SaBx8lmuaZLn==Ohq*hN*YOoj4T8T@oIv)#UMCyjVn5SN9=XO z&<)q9+@aSFU3(1{P(SCcYqrDG*TyHm9S1FUsAP{MbZ@P|u5H=oA!2EHIkJvIAzxC72&JXI7>E?BEd^^{PR9@}(k(d1OY_Lfse0-Z znU~=KU4g<>|Lvr?73?}42;>RUf|8<4E5}@$z83JR4)%0paw6d4L!r&C`InDiP>|1mX!WRmG)G6e-#TI&mKQD{s z-1C!h=xTSe_`2K~H0_)lOWOCA4-#MwYv7SqFO${gjl!_bXwZMwk7Y7EO|vG0&ZLE- zEglxj%15+Xz0RP!>TO~$+{yy_wrX{IKDCg$c( zE7*3wl*J%$=$|wBH<{bGqB`pfdip#;?>vL1I3zV5zgrU-O)Gv1&i2g)uWZ$6`G@dl z75tT1k?qR{uk~mAtJM5>RH#A}AwCpVmX}uhBJ^Hp)jOGYz?$I3;GOWDp*yqV*;_-m z!nX$3z8OENvwlJUTi znKR-C4~1#q&vp9l?(E{PK%b^QeRW?$E(m<=!Q_KTTp$RX8hmhY8vSKq)-M=nsqy$i z=a?)tgMu$i52PNr!+D3jxNddcFS?w%49efR_f@@mNv-mHN1-a{&Y5Z%o>#~h>S|iv zSdy;_x$~A9@MqxVG|Vf5+3EaKiDm1qtiQ4hY!lgDr+uQhM__y1>$#t#`7EkvM6b`Ip{_2In*pEg4Gst2eHU5nOA4zM~`icH$ zL$=L(QAf=pj(e6oPBj2W5o_+ZlmvD5c-Sho#v^us#a&R|{I`~>Od9-dS8Shole;?+;sR__ z&4g~6yZc{%5^0@c#e1z(mDsu86({cp&97lKbbo&j_w+x0-R1hFIl%=M`e0*QZGeBA zc2{d75H#%TcOFV6Bgyf29Nfh!T(!*R5w&vMW&_D_iv?W4Dips@d(B3xGNm$E$ydnD zypxLqW<3?qHUK)k{J-DXJaP&>GX4>q4B)$aw^s7cta|$-7GNb3nCwlQPDkZi>6T6M zoqb|O0Z>8HdK{NLooH4jmo#uZJW?nzJdG1$;N}clI;XL(N40HG3E2jrY!?l zXF@KSg)k?fAh~)Kxl;Rb-b~vteTrnt#4?yEC!_hKNm*nTXH0{rm?j4~4&l?L=9HOk z=@~PmWOkMv1?kY)OUlqfIL$g>n_K6OC0**W1F7Nr*y?@b!+;Y%-wwVmQ;xWJ35E>z z=8{4KKN@AOTG(xJj(7QBf0uU_8t?NSv&r8c9F=)VRQ+Q%XZ-a(ybu2CC0ICs{oV%( z)FWdHm zufxp2C7QXAcZpX64}!}9;>j)j^_}%PWM<`x)c%2Kg)Ead$r}UmWq;P&*cR;4m{iak zjws`yOkFjTYqPc*LY}PLrxclkU*_~O_)KeOQ)YKHrdLsxWLJHdkh-zpo(r<8jzK`c zIU8D6brD7V#^n&N0H54kw2p55>$b0dqklDVa>7wyG7J##BqToZSt;oKV}Tr2@pt|T zk<8+78`Tr2*^Zq(+*(Psnl0q_uuK1ns!EaodHkR%y-*X0)(CYS8o6)tQq}>)5)mB1 z?d$j!on?bS$2LL7_y*(Cjb8*ml|Sw=EafAuXT;Imr=ms)WlbV$M7U$w=}XNP94FRO z9&zQz0-o&~t0WdFUb~ZBabA%a$u$4Rq8Lc0T|}KZ5mmCY@5yt5WD`E{_HCSpRy&mc>z5d$07d5WptbR|nm7ZL# zpFV$A`Xow{i4my-az|IH)!ob*acO4jH^URN+e?2#+)7pIln}P#+Y(dG@-x+R zZsZiG9$lqYiM3YBp$Dr{#mYF%5oH?27wg7)LcfEm{b7e~?TbRZ*8c`6zzAMWJE_D* zr8b$yn~yTSUh`A#lrzrq7a)+uIp8q;w{#gYWx2_vkG^XCr?oaAAzO}q za`iXRAOqAJY^yx)(rAsX?K011%@iBqq^mq^p-t8bB~Da~miA!1;(9%(tVWsVw4fK4hEjjyvJG2)2R zYMbp69CO@`$I9+n>mJp;RavNLjM9h`t|m+AyqDmWpSl=5KqX*SV8s3aOUTxCh|*uHYtm(h%U|+Cd87T z$*O)}=cF`xZgc1Ts%)f{Ll94hNtCN6E2GuGpE4R-@DpES3x4b={pLZCR)>K0lzgtF i`v1m?i1DH12|Tq8BWjKb?U$gb9xn{0_ey~ci2(qxKBM~p literal 0 HcmV?d00001 diff --git a/src/resources/fonts/BaiJamjuree/bai-jamjuree-600italic.woff2 b/src/resources/fonts/BaiJamjuree/bai-jamjuree-600italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..c17f7f677d7665476bd1b090e6394824c9625534 GIT binary patch literal 12076 zcmV+{FVoO>Pew8T0RR91052>64gdfE0Bg(u04~!20RR9100000000000000000000 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 W4E_xnC&uIYEUv}cyFzP2`V;{7B-3;N literal 0 HcmV?d00001 diff --git a/src/resources/fonts/BaiJamjuree/bai-jamjuree-700.woff2 b/src/resources/fonts/BaiJamjuree/bai-jamjuree-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e14827bfc9224f1ba95f5fb72c4fac09ba67ceca GIT binary patch literal 10656 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 literal 0 HcmV?d00001 diff --git a/src/resources/fonts/BaiJamjuree/bai-jamjuree-italic.woff2 b/src/resources/fonts/BaiJamjuree/bai-jamjuree-italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7eb89d4f93bd3babafa88393ab810f38410f37c3 GIT binary patch literal 11908 zcmV-~E_=~;Pew8T0RR9104{_84gdfE0Bh_304^#30RR9100000000000000000000 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 KYf?v*IsgFmz^hvT literal 0 HcmV?d00001 diff --git a/src/resources/fonts/BaiJamjuree/bai-jamjuree-regular.woff2 b/src/resources/fonts/BaiJamjuree/bai-jamjuree-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..8d611b5791160a0a77774b6e91a6542e4b0eae3a GIT binary patch literal 10632 zcmV;3DR(5eN$QB;HaBgLVJ`HUcCAhiC*K1%w_4hF}{3RRz;+o(MJ$ zfGqG^qNvSL6&;BXHVzO-HVOVeB`1e1RD&O4wo9sE*8~Zq1TMY1(Zo(jgj00xPU)&|4$%-o=!F}NB6RD5#`E7<^SK`-~iq*De(4Wn_`00 zg4=IDtxywwMofzdJN@+lOX#kIYJewh6mg@pFQvS`j8X~!t5UVu#Zt0+c1b8vz7*rv z8%RPYoK4EmzYF>25W@5H)JL-TK8n;%wv{!@jKp|g2N=I8(q?-U=72yE z_y1HaTYSyV9dvnCml7|Aq7$4$7gBe27lDrf2ZJFNjBA0Av@1&9$IawMYULo1)C#0@ zA;rouNRBafuKZk7&PA1CT|2if%FdPRgZTbT&7Z}d3s@{ZunLtp2mQY3cmOk{VoYj& zT%fhJ%r2%vXA5Y))y%AY)2Y-B$Bsr6rnVC6VWAkvj zXyYdaU{Rs_A5ef502GZB28j>>F-wJ{D}WTKgEVM>G--jf837q-6zsCgkgKjiZnyzK zz%n*vh!anC2SNCCl@rH@pJ4Z!tb{ z{ToSjts{nGL4xj=zPL01ftAdnahI|Py4H49&&!H>;IPu6)&>}Ga|@u{4aq>{wQTN% zIV}t+F$-G@uefL;v`jukVN&P8P2=qM0r`#iD9Sg=SS+RBMYo4g>KGk~Ua@ zAyI0m;rK@Ei(xz+VjIM^NZJ`W4mtukA>q`o1ibJW5-dJ<5e9`|Ai<$`8JMXH8db}? zy##U;QhI51tJYb}0wK$+#>!=91#MiGG!@SYjYC>@Ql7la=lBY)~CnXECuwdO4#amN<2{C$JdXtul)-;G%iK>WK5g8jx zN)@1W&dy;x9GODR#<@wKLncz527R_EH6gF=+ILaeRZ<3Kb?ae#=9KO4uYc)-9>*%I z7V;pto$SA}tB6=}5|eR`u)=)z4*a}j8H+1UDsYBuv_dUZ+Kdq5q?+2p=}S*oPfx@L z5n@D$4#kKNBStbuBK0K2LV+l;=%kzx0umWYl+Y+q;ENJpl=z~8iyAFFgs4Zpu`tkh z15D80L>ZIpVzLHP)tjcD4mmp2*&xTx406$wM~T#VX=><9Z%0iHsl|J)6DI*FQHdnU zk(#2dL5N3679?dWQK(c=Vlp5KIo08)M;}ye#A$J&C|TgaM*xKkA#$jsktY{(!+o$g z1qqgXvAC65s7kM7A`{25y>-}&vQyFd0pk48ZI&r*zfLyZ!HisNE5;49Q!C%~eWg$i z7B0owQ2o?Bf3|1p6#FrrGcLRVTcRX*Sa7E~GUVH`>$@ColiEZ`g7}xAafh*NM;2=rZT3&Cvo*#uk z5xki7!oZ@DgcN8r6txH@h#-QAo*;q=CIle~he!yQco7~^D(?uQf)f!C0g);J5F;@V ze-c9YN*Bb4far*T=!h?&iHOLFm}rS8>|&f^G80&kiOkPr7HBGCOk)8$m|7=O*}&9x zDoywzK14+*A|%v&LI}-D3gK`JjQf-!L24l`9s%Q$S4EHm>j_YV=wh{!h$&jcu%QCFk?l_Fvt*F?1bs3kue!bKhLos1p+O0vU1;OAch#1qjg)Z6?AVA`c@eo3C&7MNg7hl9L4ZJ+g3! z(W02SH=d73nJs8o1`>Eg1wPVFMcoHsz4oy$(bE=l>OROS7>kS5G#(X45(E>ZuowuD zLXfeX%aw~@NQWU9o~pRQ*NkY2QoZ`&Xfea#BgqbPCkI4@c=1ZlG!MOlAv`WNJuC`r zK*G|Hk1Gm62j?u;>J-Ni&4(5&R;5Iq3~{;%h473u-XxPvG1WBKxGAPgYz&N$(VEF7 z&X(wtN9GLL*aQk03MlCKbiCfJ$OdA&Nrsm-1RgbX!g0Y|Ks~rk#Kmt3+MP!N10@B2 zR_shb0(*e2YiX_paVs75all?#1t9o&BP6E^EDBjUF(0CbDN!X$JhgR3{jQ5KicLz( zkBrf{My+3LBrLm0GDH8o2jNH)$4E!IS${3UPE*NWBW;X{ajl-Km0V3!lI1UW0+NB2 z9A%Z7uJD&ZElq0t^^@c9x=b+16kMPJPzZ3QU{cB}17iqb6iQo7YJunPq1nrASmxn? zKGO(r27i&cfHh%&g))7y9pgiA!ilb2a0cw$NPL+C04(^t%Iyd(1mu_51wtqwwIKk4 zksb7zcQ^(~3#?=oWsC#Alrp9)e8 zs{hxp62l@|heL1dmeu12r!pEwch{^;)GslcPk?g(Od;U^gL-p%dFOu%`1&a`-YoSt z{~P}KoO2t`QLo{vbzdCyX~>vEu9YP zu+uv0J#f!`o4r7yiAcz(dC~Bu*9F7CU!VwP7F}6IVnvA-qnG`5IpB$-eiJ82vQ+6Z zWXh7QP?2J#%2a63q*;qL4ucFf#84yb^~pYuT(Ze!*IjYLXRrP0jnU3{?2xy9_ufPE zE%1|9w))vTW1O|ZQY&r2$H7J65fBnnkyB9erR76MDnN)J!NM5b5vr$dy6Yj9Nh_Og z^p@aT@e-xz$F7gQ3gpU@FHN}`m8#UKc2})|+VwZUFpY)_SMRGYPC4zE6HYn~LG%9y z_#2R41HXbbmjP_}8Nh<`K|cg20G(NBZ_q9n7FE?u$myBGgwu8>iWXL9-mLW=2lvN- z6L@~RuH(>pDb#Wvh=EF< zhcLu=({uPt5izPy4FESkRYq^Qg4?y~(_>@w$5#S3#2RG`_E+)1m?~eP$KuBL3B=W& z>mRH3+}3j?`frM>S}A=g-&T!>dbf>%MOeQ7>3q$Az8Tf)n~VoF-8Vc`R@Zsi%5|dM zSbNh0MqO*Jh4mYpTfMGsGOq)@SF3i^IKJto{(hxt(q(xQZoOGQ@!)lX%4Eub>hA8s zYrDD^Q^o_Y#<%vE_!-Z=ejce}O#F3vEF5HV&9!QYh$%F;xv5NYWtW%pTKj#wT;O=XfLl(>>{Yox8o*f8XJ z@@!nGHvLVZq29>7Q<~4{T@?VEiBGgJF_%SOVXVz>7GcgP&R}Ya*C-^i3yRXe+=h#a$T);-@*nsKHJkm@yt{oT|D0P_2nx>!(vpVf z;1{;E03a1y0wiFdseqC(g_-1e9Ntm5VTjA@7YElzn91*UH*7pQmj zxed)mC=6o=SwkY#S{;p=*E8u=v?QK;PYXk%hVR|Pl$;%bHtnqa z;W5SBC=d%)(4FgK$N6Gm5nR^InA*8~yOOpDztetv9xt)2cUqg`vlsm7*96JN$b^WE zy(J-c05QQ*#{@jXR2hFceIWpDfo>A%5KeVj%5t3yqp0I?@P|U;IL7MW!UBahLRpqb zxXm51gFdT7xM;jhA0@D@7iGRp&Mxu)*pN@6QWG()%aUt8;Z-jzVVX#c6En5VvXrgJ zH_5kmBdi>>;pRC$GfI}?kDVUZmhhx&Q^)LDi6wL~op+n)+7<^xTTIq@p%Qj9buQV- zQPK4-No1-iomhTB3XQA7(j(a35XFn|G9A54kpI2B_Rh7ySfvKK-wOKP$zjw?Sqss79jxGLeCpwxN+Rny&>@=tPAN52TavqdUlk)QZ3g z;mlhD*ZDob_?5fe&V-lXyk^8ax@z4(M>Sl`>p*=|=U5@*;QV+XL}=|Lk<-yrQ9pZ* zGwqEITys5 z$4j^l9~P$lXMiOL`8Bi&qg{*uuRES5F#|H~u5CaJ-v%}faL79%*~FB9-CHKnmkA0h zuei=4{90^R^TRA^srd-PMY@{#flykVLRaXSFh@JQCinWw6S%=uJQ7Uj)!%%$J$V3% zp=52?`BRo8)!`?}zgJEo3LXA*%r092tIP@Gazi?49K(4F_k8SjBeXf9Tfv_WTV+or zx4+~A=5IwiPfG2B!9U~#Q+U9-`#m8*uqbcO#0bO)LN<%?dpw%PIocJ4d5`SfkZ)^G zk>S9d33l7W(}`_9%eqyrf3O25IY@BEk_VH9;*v8WTj^@)fFm6w7L9_jc zFr`#6GNj83D^vz5r~S)0hLlcwn}cHQ>n9mmM~&JsLhxdw#SuR(Pni(PSkE+5Lti-j z3l&WJiE$yUZCM?VXSrQ~xsD+`V^=hP4@=$CetWCSTcbSn{63UTNhsEh>K= z_Wt#Tzv59Ga`xbZ2hPQDlh1Y=msk4aEWrv&O@a2&Wdx*iL3jWHJqps~HU&NpA-~6PqPn7U%^^eK#hgo{Jt;VJE+- za7Z{jKIXXG{4e$fnFS|+=9o6k@H_4*GYRTQ9WB~;<^_9C9@EKW0j)pG4k_EZ>r3*O zcg(HBr)UKP%vT)Oil=@>hq4CqqDMsynequcd;0L~Tp#9y4MIx-+rx)S$Nzti!Go;a z@%7HHLpCCX06m-3&Q0z?2|?bquRmq=srk=c|9}74Q5JY3aRr<8G=fD2%);L0r|@T8 zZrSAi`v00XR)Yy@UWceG9wfB|dH505AS10(_?zp>GGES=zoXQO2nBNXWxr?41gcRK zFI92@ILvOc>c_bby7c2?sH4?Zhv*t{==3pot25LTIv5HxIpT!B+6$dXyupOW!6>$e zU35c`T=1P7QE1bb4ox09ha;Ct((D9(1i`rB{0;#pLw$_X*$%m$IOm6C`r)9IYzpR z#^zu&C`$luIrx(u(#xCdG-VBI!+LAff0RBtwsA&97y#o9S3K-Gp*=9FB44L_ZGmqI zJ*^Xe9h4-TFAhFD=(;CHCa;9c{j*A`j5XRkRWW97uslX9 zb6ycV7>qQn;yoPjR)t}+r?D#V-@rEsT762{EUuHW<>0=hig7l3TdO0F-le-NV~=jFTetQgVG{2V(S{ZTH!3`$YUwM(z;LpZXUfNhnwx?UvD&m_4R)18CzS74n&TN9@>(!zbR;?+ z{L^t38-Shdq`@V2tzH`=&eG>8PXvo&o(OfGK1*n0wR$WLie7*7?)&bOb*xi$?!Jja zu%Wkf(lja_Wi8V2Q-Y1(CbQyEYSOgQ-t3&e)vSf;9Kde4!vwZ+w{rb=m}d=veWLxM z02n@qwb0ky*4^e~EgVdPG4DpFSALb#sPz?>v8=&Hvs#vgAh^P-u{aEA`|ipq zJ+(iINHg+t1p~$49-9)O7}*8;RT07hr`ju%29en&X_-%{CGA#)nKsWxjCPq_L%A%| zQVe7H;9GWmo&Q6oK33}YRE9`WTqpA@O%B6{+5<|zTyI0jKy{WJN%wqXqXQ|G_SjZlI(>2X4(>U0_R@(SX@xq#AWwr|6BO3>^O1vEtp= z2u*Xf-U`i8wW`umk16ZQwI(R`N*Y)LBHD2|Mi+F~@hs_BdIZaO6`DPc<+6M?E|uU$ zXMkwJpxaDfo-hTzXE)CGbFncN^!sWXgON#Q4T`DVs%{xc*O&X<30W3mz!iQq>M*2% z=}Q@mDe?ybrG;prVzahr3N~dK1oL)>ND5jm3j!KFfqE**{5(p%!1u+(vZhf%GS@DY z=*j=0)s3NmElEXs-5n|%lMz}gWtq6RciNSJL+YI30@4wzbeF<_>|`M>cf+p?5VTm% zWM+PEA*A6W2udH`I!0ss7Wi|kP_qN;1OIFEgrh?6{gzt<@h9wp>z+P}96xp~8@$&` zX`rU?f>3}7z~YzgpeqV1rDO99lBpBiFf1o>4PuLPssTPCp>|E4sd(qp=4XW#;Ce{X z_0-4$Vul)4p}s~S539|gw?j=G_$^@0=4QrUYSmc@{o8t!HGU0xeg=zO1NLv5lI0M0 zd_Iaf|B*WH6t1I~#0yf+8A87Cf%UR;+K7SGmEOy+<+exnXyCu=X6-o*t^Uu0R(lH@ z&b zSJxQqsB9P$fPA|^(hK|cYAhbC12N%Bve=Go{-VKdmkVhu3B!x}Z9829Jz(4EA(AW(NW&6;wji}*9$2Zl+cPAy}(-Z zdA8-XT+=ADybEr^6+J6jg~6Bt+hnuQS>H)PKJZvE0o_ZD#9Bz8#1~G_UT7%1AOsVc zZfUyoibj?)_oa?|Hq*d8mV!ziYYm$JR8K6jYq!#i|KM%zO#f0o6jQ ziB-A+Z`-%3a!h>yHK>@V+*|{Wvug~R7RSr@*v=ZpUqAFl%O;zOL8)*_=Qrg_g)P!D zs=Uyusj1dlDQH7oKD)?dg9vU4`MdJ1l-mNnOVtdod$f6OJHA!6dwphVot#61pV{N< z{5dL3iGej)OiD%Wctsgg7O!xNq+)V1E2fU+_{Y~(w^qN85GJKaX4mT(yQQ;kudKBRRt$IOrh7vPs4H~OWi)J6rG-UQN%c}M*5}pDeLmO(TEE((vT)a>8@Ttf ziZcGq`tm3X+1wOCY(_=i1ZX5o}062Mrc4s#q;#Bwf!GgeF8p#$p`!0>W?pf z+=K_8z$f6rN%5Vv#Xru>%a?Vl7`+`wV5qGr!UP+mu6*Hd!ROEMPx6%X;JM@s>a$;P ziO?n>who9y-*bU=RGaTDUghfJ;}wRSCS9 zK2P@~_JD5>!O1rv5Y8~Rc51DyyMr^c&Ecr^!e-*wG|t3N4Q%&VnZ4#}F>0&EIn_Otle2gbem9LYE9X_A_UP{-=Q>8pC9|-^W-Q7jx zEdhV*XF{HshY7g_m_YJK9+N$i2y6l%d>>@8nA{xaJfW^w*<1jSoPpTDf~8<7D>|?a z@JiF^HoPm+)9pmSW>_yA2$R`;Ba7xOTEJNQ0s{c4G$wKpcFV(egdRAgRmX7vNxa%}x0zUy}IDas23U0y?JL%4q=9R(0PtT4&%DhbQ z_aXAfnHMa>ntrLlTPdP zX1|yYdI-059O`fzj5?bgG4uP6F!IpCaYZ>iQ+AfnVzNWTME*16^~3X&i$#Umt{be3 znh$tQXiihh>U7^X=r;#)k}?X1eUdh+1N`u2vu22n#BJrhX~C|0H8*bbx-4QyQcIn&dj$amq6w-<$Pc(XrPNR!{-|28OI*bmZ zgADC-gE;FKn-21kr8DSEI*ZOGbLKagw++DFW@NKP#FiwVS2Md3dW~eGg_NJa#B%V= z_J@Txtur+%Z(76_ECazTf8zvF$mZrR1W$B4c|aG@QRMCpi?`4$vWRtnsKc{s($|sY zh>2g~>hzl+2E1&?6bO!(R?7YvAf5;2J`bRNyzSbST}FK_2m5}ocknx_?&k;8eX#iJ zBhBe|h7SB=v!~w!e%KA*zsbjR2XM%4pa#8pYy{oi%~7WZ*$5f17YE%PI8ZGDS%}`* zcQ?oMNqW}nb5ZiHy>C-bobZw{LhSror$cmiH%Fa~V3#qU6Wtv+Xq6Q70^1|H+8&}; zyZu4*Hd}&)ryTTZ@CWwH1ZrP+1+pr3gn0pES`09a*x%B#fx+nk5`I-GS z3l0H`VSs@Ck?g{AL%I#D#Iqy)o&T~bX2J3v>XX#ZbF9Jd5*b}2wv_9<$NwR13uFL# z%NKS#_IQEd(i(8hK**nj| zh5CO=`u}|YL(liV+(dN8(%F`Ka2kL?t;)Cy=gOZzf4nYMY@ma)Z6*%qc&vG|)lLs&AADT8S< zk|}AJEM(HmW5e9O-q?{!FbAZN?kCDHQ?u({jCz9R+Izk8Dv`>fJ==|uYjZI`JJ#F% z2l64Hc+Xg|7Q+A-kcyI#sPb?udOHDJWjSb-%P_E6OsQ~grc`*t1*F*22r!}L=#Gqr z1b7Bu4?bthZ_susPS{48zoN?8B?uu5L?l)P)S(a-YJvJVQ_KqJqrvuDZey#-W_}}m z@JksHL6%bVi#+Q4k%7T%gXwSqV(z(z~9E<8Hm$6 z^@d3{pqZE}FT1 zplSm&`f?C6lYz3ue(W3usAtws5<8npjnY(V)nu>%YSoI6rB=PcT2vY!Pptt?&�= zqbn0j5yfDd%Q}*SZRpStW^D#VMneokJ%qID&dhHD6L0YB5idbHMoiea*k0rH<2ke& zevP7f4S|`J<)X5fSzx@*{=m8U3N*@wsI21pY7VW;Hmp=bc*VjRwWHOQnwcBijA~tR&2=F{g$Z}VO}E?@!NeTmOy96N zR(IT0@4oqV+ar>vgY5K76+WEv*Ra3hT}LZ{L8%J!Wcx$;bxufQ1Nj5oH=?~N1obXFhqVTKV!ig&Cc#)CtrMDev zJ%e@5n@WUB?i)h1G8N+VCXL2E``KJ@(Scs)Xsc~Kd0(j9B#qrj{X!{zO-2%G;FYr@YA*Z6h7g3a?W;ZglA2b^-7+N;BNgi_LI9G}RtFgnb}; i`LNc1on-~HtyM3``e!IO8Xx>|j8152so5;ZJO}_6IaL$@ literal 0 HcmV?d00001 diff --git a/src/resources/fonts/BaiJamjuree/font.css b/src/resources/fonts/BaiJamjuree/font.css new file mode 100644 index 0000000..4e239f9 --- /dev/null +++ b/src/resources/fonts/BaiJamjuree/font.css @@ -0,0 +1,97 @@ +/* bai-jamjuree-200 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: normal; + font-weight: 200; + src: url('bai-jamjuree-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-200italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: italic; + font-weight: 200; + src: url('bai-jamjuree-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-300 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: normal; + font-weight: 300; + src: url('bai-jamjuree-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-300italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: italic; + font-weight: 300; + src: url('bai-jamjuree-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-regular - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: normal; + font-weight: 400; + src: url('bai-jamjuree-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: italic; + font-weight: 400; + src: url('bai-jamjuree-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-500 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: normal; + font-weight: 500; + src: url('bai-jamjuree-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-500italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: italic; + font-weight: 500; + src: url('bai-jamjuree-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-600 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: normal; + font-weight: 600; + src: url('bai-jamjuree-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-600italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: italic; + font-weight: 600; + src: url('bai-jamjuree-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-700 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: normal; + font-weight: 700; + src: url('bai-jamjuree-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-700italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Bai Jamjuree'; + font-style: italic; + font-weight: 700; + src: url('bai-jamjuree-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + diff --git a/src/resources/fonts/Space/font.css b/src/resources/fonts/Space/font.css new file mode 100644 index 0000000..74bf901 --- /dev/null +++ b/src/resources/fonts/Space/font.css @@ -0,0 +1,7 @@ +@font-face { + font-family: 'space'; + src: url('space age.otf') format('opentype'), + url('space age.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/src/resources/fonts/Space/space age.otf b/src/resources/fonts/Space/space age.otf new file mode 100644 index 0000000000000000000000000000000000000000..6b22db02f439e426b721c42861e722571c3f755c GIT binary patch literal 18496 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 literal 0 HcmV?d00001 diff --git a/src/resources/fonts/Space/space age.ttf b/src/resources/fonts/Space/space age.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e89b0fe64eada100304592d59b4313e9552e9771 GIT binary patch literal 26748 zcmeHw349#InQv8(=Fp6$=bn*9GozW&XmnbVby&i(Wf_b?#@NP|4h7xgs=oT_ySmIcW6a4eVkS24n0a#-{PnkGvl(0QI%B4r<}PSy+xpx3 z4;kZ2@YLX<8OJX<`;ARsW31#E#?1ScuO3?atJ_|QGiLuTW0oUNA6mbbS@FkK?8WnC zr?1*{+8=*i`Z#0m{fr&I=ZxW@74Lq${kOP>^4X*_kl}sW^)|})<2rT5>J8_f_(H4> z*N>q7vQ=xA59yN=n;BdBAo9Mxdg$D>>}CFCq`UEad}L_#@P+lK?LyP0FsA)(?V9x) zTz|H&V{CZ@&-{Q9Za5~7E_~}$*OWiAQWM(2*rOlRt)TPM!?otoDWkjG$7%25vXp6* ziyz)G#YfL%748dge~SA!QIfnT;nO1LY3^ZVYy~SBb6>-?$&}>Vm>K=_ZF4({jLEZU zFKc55(OTR>bs8(Vh_O3Rs2zv+t=+V4J?mrfL{hY&%@oHQjCtrL&+rURai;N)=o&dV z^PeKIkF}ZB;kXRPcGl*;z_`BJ$fNUmHjzDp^OeTE4OH^z6cgRUJEqk*T8-mAoT;p@ z;~9E~&Q!-oI8YBq8SWKE`dFOledL?Y#es4~n##pB-cOm{XBUV(dWN2*d*8yb7zgS^ z*-4&4xdXVKg!75ws4$h_n%<%3>9~xwi}QM;T?!X^hT2RxQ2BICXR3qFgcr3PcnG|% z9{rORK^^ZIdAQF%W^G!zJP1GY4%{PLP*+KhJOh_B4j&(4X*xtb)KByr4(dU$XB>gKGJyqUK}su zcuC-7jJ?3ocvp>~XeZ$qWnT1ggwDCT=snq<{U~d{syBD0w$M@h+*`T(s+~5ba~pG^ zPjwvSqCaTtF`o3k_Kt!3b~YXFPS++dU6nz7s`@oI)~e2Rc%P1|NB4pj|)*D@D4D*#Vu*0jRSwK$n1S=0w`VbU-h20s5F5&@W(s zc}BA=$h?3d<^wEee!vO=!z_SwB?|&ZSO_r6$^l~nRo_DfRkAZU=M2roFd>t$_#(^xxTAL{^|F5r=@ z6X~PaB)}Q03-D;x4LDQ4W7rW$9}AXyi1o7`z*%ex;Bf-ZW>b+qp7jFGVbcKTvOd6h z0-nI8kN%C#XGa1qU`GKCuo-|01zg0AMtU)u3AltE19&1k7VsniPiFl{e~HZkJcS(x zxRlKXJXOF!c0AHUY!2WuHWzR?n+Lc;z+ra6=s|WGn-6$8TL5?l8vtA>;F)Y8(r2+n zfUDSIz}0LC;D~^0*ojE5WhVii%}xef$G!x(Uce3P6r?w@rGV$KQvtus1_93%a1$FE z{VUtdmI0o}mII#8Rsdcg;Du}$>94TU054*v1AdjA0r)imx3HB+U(C(~yo8+v_z!Fq z;H3h7ovlXtGByHuIa>pG1zQVvrGQtlvqwK+TiH6mZ?N@%+t>!cZwk1bZAAKNb`IbU z_GQ3p*tvk;67X8K3F+(DX25T=^8l}B=L3F6z#G^FNZ-gV1iXoT1@LBe5#UY%cd@UI ze#~xRUjw|AZ2|l)yBP2`0dHrQAbkh>2f**KO9Ag>UkBVR;9cx8r0-^z1Ad=f0eBC) z67XID?_*aXeLvd@_yGF`;1Ad~zy}34!}nRe2iU#^yBPX zfKRY%0e{4<1N^aoPqJ?#{S><%@F(m$fInq706s0?GwepBf5vVCe3sn|_>XKS;Bx{# z&vuRenf;vI0{BntR=_>%yMQkU_#(Rv>0hwh0r#>y0DsB82ly)if6eYh`X#m-@Hgx( zz?a$Gfd4GuEA0D7zsl|be2v`;_%G}}z}E%*ExUj8BlZS+0PuJ02Y~z7gMj}k;G66r zr2maQ4EXQthk(Clj{v?U;M?p`r1!JO0RO-q2YiP;0r;+f@39{t{XY9K;0NqUz(2C5 z0RJT5hwLY#2iSkGp8_6WPXm60)#L#Cvw-L)q(5fQ0)E2&5%90=p-d{}WT%z?G04{MAI{N9cAA^@q_ zjP;}nT-pP!9m4t$VPUKxHgN9*RvpIHfWCi-Id=iL#|Cx-=G3|17gvGOAHi(85%c_F z%%q#y7I1=#FpqYzP2e71!7RF!eI4_keph1FFmU{DfJ@zn+3|hM?GfahiFu)Zt5l-k zd=_TRU6^b4=ko5rES!u!{U*55h2V>4u#>@G2f;;`f~(T+Qg9W<9>e^-U0f4*2=k1A z*ImxGgDYMQuJJYS#cRMZ)*=5|aEr^pHyC*0d8|a6gw!OAObL4KSd7($DCqE3csf2ges>~?6`DrAALF|dt-Z^5?@7?YCwx91 z%)RG}m&YUGy(83n;7Z1C^!3qKM@jz2Z!|DEH0sOTo4NVW!~2eA$-I z^hWKy7_;mx%no4{O~B=;sbl#C%ZFU7&;Q z7}xU!m7Ym_6qI^7#`qdzjDG~Wy;h9#g`nRDFv>L68`zT=$tL*_V^VutFyOaTC(~`Ia5zQBCV5K9XZuZk(6#Fs^WoV_}H9P)x+HEsjjA2?S~;Mlzo`y-}Y(;!QM$HJ>XKa%FDwNB9f=NFdc3 ziL|Byn%}k1ABp$}D<&j-0kK;Vh0j#5} zrma`&>}UqYNgb%iwDuZ-f3vAm;2kq*S0|e)N=qx6lG#<&jg)Gv)`lutXEbKJ>-#2@ zQ;I)FsXHrDjb3kKszRMNr;ndjr`7k)u2Bgr?a(*=3QNpSuqIE&jK3Rg=;-3T90L{N za;$23Gp|lsxel1F`HcRkkz-?Yoq>SrH@s)YA@TU!fwjnPR^y*ccZwkp!*2PqnTI`n z4YkueJZug7tMu##=5jse%>L0F^7y0HbO=Kck88L1qfvk60u04wO8xa4=$t0=Ba}XP za&=?4v^0#)>-I*Y-pp?5vX4DcNlH=k`8lHc*y)WF5!J!-h_tk(NLwlpNVP>WyQoBM z1?4uBI<-lOhK59Dk8bxzE46y3Gn{!t_ea|?{Lzym?n;b%o6Qx)=`|V$&_T2!)!_9u z)Ra?Z-LyFl5OXES)&Hqv>vy|@fz-rEC1{!o@Zs8^W12q>$XB2xIZ3D|O}W`*vBtQZ zQ~7~ZlS%uLm@~ie=wDIO%A@NZF|`gD`vb$;;S|aU7|^h)*>dL#RiH;kUji1h1} z&r@0@noDzJbPB(bJtO`6ipf!5Rj#KkGzj!U8 zCTvvHFPAQA>u|U+fwj^XebMFD6osoZF;QN$l!?hevtuHaXSBHoZH6pCa}!-Eheobu z7DJve2r|TDSn(!xYX3q=ch&2aj_f2~h0SHlUGtxdYsn3=zpd}KS$Vm`nNA4_fLBvS zc0Xn0XiUp$9%!7bOR$)ZE}vXheZ+y0RcoSk)ndKW74%2!2Rz;J*d)&Z3#u&F|6mO` z90AM8=mD>T9}|j(L)piz9y>Sy@3Q%PHli7ff2WoOj#ao2DEeL8B$jC^TTJQ@Y6E6n z+MS{n{vY11c&yV~Tn&F$){}jVD&j}Win7nkim0A`(}k#qRuzMvbW|r@QX**K>FVT| z?lda(wF`^`rVFVX%_ZfpJJZ2n+WC6OY^I#%y7IL9EwjsJbD4k7^}6!9X2B3%tFJuh z!tY>Z{cALwJXP0S?#UiBI~-euqC|vvg03SuL??N9Yk;TU(EA#EOIZ`2xjHeQF#;WjTJz zw-83N2o$vUD7>t5to$KGdRPv)Zn6qmq2B4`UCq1@5lrbkCZ?m>7p2n!orHqNBQOXi zx~DIgVsnEc9X7hVu){!b>cCMw)vi2-{Ei9JgTd(&1ZFxR#!KV99Yp6qZ9*UJH5*t@ z>a8lxVJ+5h=nniBkY~WDNh6SqF_{h&q;r~t(kS>h4V5CBQ!R|X_$kH?Yqcy%u6~eh z^D7KiLqa>Bp_cMC8buPd_q*L98LE$;O1z9Nh+Ab|7Ccl4`@2c#j!df(vdC`8bwUD{ zSHxp!>CV$HtwWPRPh$t?(=;BM)j7pTd#JKI>Ohk`p87?nteEtVQUUVCo5Gq`DL_h= zBRTGXr#55@=>CWv>&7#OsY5_V37xXfIIYLEHu&)`eTO47$bN+-=$mSRcdSgGtPr8L&4E!SZku`zd#WDyst+Q(_*CytBQtI!RsM z!$Y0zfdq~kWFbwL)wO||u@4J@v4@-SL>Hduw9-TLzA6$kpXaq1Mf*aXv3zm#8PiwS zpWt)3d}pnjA9s0HpB>9Ah?pu)Km9=Ul1OUFq1TPOXP@BJb?@rTQ^~f8smkW^mXf&B zcdE}R0ROOrN8G&)wS68QGMS_PsEf2Arz>dAerSbO6tPA^jmbbPq1_~S-bG%gcCIfH z@%{BWqF?iUls>q;y0N^pv>aMdyLjq8qUw*Mk%^?~OpKVs&yvn>l2w@Sj%j_NLXudxLl6RZ*^a!jkKzhDt$4C zQLPSeOMm3m+Bwr|p|M3PYM}o@tD;P(2o7KnRa#YaV#=@ibzuxxS}`H%_j}v{zuP11 z2+*qh^in{FR;Bnp%~Py@zA+r$8Z#9!)gl2q@Xfb7R-C%HjyS#B5pHd5k4D>DTf@eT zUA%Nf$J?Y^(!m#0Hg!jVfgZ5=t78#ekHo6|`ME084ntBl+5{`8kYRltX-HbM%7OHS z3L*rKheNJr8pR2u$`U^O%h1ROFAQat?vQn#)^?KZoRA`qK*5IWDVJ08SV_;YN95ES z7$h#GlCl>Q*E!Y2<=&LrlhUCpq`Fbr@2q0qF zC^T6>2Kg?!=R=wdnH#A^+ESWS9#@!{@mJqVnwB)P(uO z{4bzX4qu{slA$4QB$7F^j3Mu#^mJ{ZvP&9d<~Cs;@sVyz0=(Cm`9xV$w8g*g>;bFZ&+NQ`Ga(BgsFu{rH~2=42m>IH2B(j*iQ7D10Jp- zYpc}06)gz6D@jWuk|6vMWrTf(>JpNQDDMLBhP7fGj>9;J(eL4%z0fSuwPq!NiV2uA z;s}cCB{@nkB5N-!m(7MflRv6^d}Uv8=q`KqHmaI91K*--JJ}CO<)aa}gvQDguM0^- z=dINZLE8Zu4i28J`%M$5nWsou$}s)p#z`dfG9FIV=d7QK&k-#<;iH^NJj5`>2$Jqp zlr1tzYsFN8L|kC4%o)xME7`|n2or=}DokRBh{Tn-Ok;&t!c<1)u#brBDUrRK#xid* zE50h4dr8ofWX4MBvzl{adyNKf#S;HL#28Q1R8Y*NItoy1_iwNUt#%$)|5S1W__ois#u(hvzX zU0lk#M*n6S(w-uo1%E)!)I6?m)~0E^(gTSZQcbV~DkE&XA6P=V9<*dXw2)NOXm(h% z&UiyZoP+}_3_)65nafp{`JGeRRmA$X0^>dr_2rf=wSeUK**HA=hh)p7Ueit`TrxL{ z!LA=OSA~3YAjiFE)j+R~ov8+|9zZ+DhC;kaHX%ud2&H8dlAOWMj0P58P11JKsz9xB z3Arpgt&rUKFX>LUk=iTu!R+}&qWoD`@C9l!T2$1u}!_+OjJf{@W zdj2%~555J_1JZt`+!COZHdkfR@2jqKxhkuD{$!<#R~KI4F7*xH&Xd|I!BfNvnX_RB ztcpE~cJd^(i;it7!GB5_LaNIH(ce9$63~eQF%F=VJenpc>PSwDwYW&Wpf!>PgeMj5 ze+t4m#($y~-t2D#_ANvd*$!O{vEur?LXkS1sewepP!92mF=y}NecE+oXEeizERB4y z8Hgc(nS(r4A)-<4hDzTyv&rvon%QRaVNmqesVSG9>TT5{^BlIxC(fJG(l#)EkuVnc z^^RG6FotwbnLXJJL#e2%V)Q;o zo2uCAp9iysS|-==yXWysmI{@9>R!^_yEgvU-L*pLKIc6rI>bIZ+kO-$s>Z?q=m zdQq~gT5ZJovU`?XSU}7P8~2jJA@UqIE%| z(NzwMLQ}JJY)pkgw;C%~E&q<P^(0R!ntO!>Jqi) zW?m&{Bq?vRXh9uRQetkdDP5QF>lTus;+87@gKU|Y&5isp#~Q7NwFTrcUSNoDp1-e@ z-MnutfL2|lh$QDqR~(nd<#68`<<2jXo%&5*6uFxE@4r!Z8J`pd-T&|Nw|%jE71B;f zzONc+Qj4+H93}^nze@;$LX|6&pCB`mIxS5Bg-#|zk~5`CEG10?N7mIHInab8v^ZZ? zcVk6GV|SG=9LdgBq?(1-yy%v{=*51{OB*I1lS<9(X(;7b)O{5t)^MW6=SwHTmXZog z#@7rAeoEZ5gcE6>uO<<;iktL$&&*Wnn920I!RIJ0jA%>XVE7dOhy079f5KW$GR9o+ zRYOIE_tvl!k>#kCVpYh@3rK%m>blpIf6QW4Z$NvbzmPU;*L8cg$I+ZtF8`{IhG=QG z)v4PXIB%hIXKB2#JE1}Z(#;P37kVpm5MTH>bh}*-Lx{Tc?YhPIQ99aLC%Abu zK+jn%7Bk&cGWL7?9qkF>!vozEdhuiwiWF9)ISHyoEh69Y`kru063S3D$i3QY%Bvm1 zp&8t(h(nz@f-JD$*%P&OZdYAx(k`5oZhL(=TyKY~jP50C>s;=-+Jte>LH8VP-e6D% zA2AZ|!0TtRRfN3UQ(jSO!2%Z0ZB=e}tW*aNMMg!rhkHWmR%y)buCnPsoYu$#{4w}z zo%ykpeqw$(tqDdjh%kd20UdJfr#a?8bb66C(uH-Q(5+0QP~dgumn!6Y^L|&`wB<_% zTH6;cTGm(KdRA-wpN!QV`dW8?qu0~a->qEFSo(z?+As8e2WcyXv-#5$ev42{Bd*~gqksFxF##0DHy{rEXWEb8JtVrBCc_e|5UU|*riDv7ft6^ZqcI$*D?3}- zh}N7XY(sntj6)xWB8kv`X&UJ zyN5ZgrEGg7`iNp`6d#o=3Jw)9b|Ixt@ppKgn9HvaV>D(o1#}v^q>%GA)9jA&0-fpr z^tB?sAeRXv$V$XD{@vi&1zWTz#n}) zC3H(FXFLnvc2$nD>hWuG<>ah+;8SUy;xt}Cd|5Ppiy$Q*-pr>}@v50Oj?Lg#=IYC} zyZi7glHrTnFdjCc@iK2yRVoy7ZBQBG;hb+nz8ef#h4d8IC#0rWONP2e!!8K5G3Td| zhFkL6S*!@ZAk!VW?d)!=#ZHAcCSiGoF2`b_K5M<$Z6YTCP?RuMA}wJZ8? z@xr8`dRe8VxzSi|Iy-HV=q-1=;fo@1LhQG~HhxU-aZJ}zDGFumOpd#f~xSyo?j&xq-JCtrI zk#81iZB~#4LcWO_o))l1(F8Qz=@ZJurx69zQvA^b7cQwh-3Ts6J(8VWto({-F64_` z<=#@6xxN>;_DOjHp?1nA4l}RBB~R**;6#vW5v}?^Q^dgzrXDVD=j6?0UCQ+I_)U z7uO8ZtLqKPm~TN`j~KW;LRWP3kuNO!T%jQRzQ;#*wA!fTpXTc^kAmb6>LSJ|Lhuy% z2n@E$<1Ru!;OT{>;LO=CV@xu~O!eidN4? z2bc|lj@v^*-}BqDuX$gSZJlZsJR=zRCOCn?+eu#u;#=Lk^;t4^)gB1gAom_{+Cq(` zD2mEra9AT+Hv4a79#5HeJ;Ch1Ww+4ztmN^{1zJpD0PV>$R?`#0j2-rrQM$}CNFXq! zUgd5_3aEj|&PaG`R6{MGk*f z32nJCy2}3}>wq4$OH1QBXNK>QNAPVXlli8_B7eD&{}-H1qtAoaX0=BJJ;Ux$Yt{sEgD`?*_q8%IY4ZCXJplF}WA7Sr}$wy3xz~@gm zbJnl<`5NsN8L2x>gTh{=39goeo?V-Mf%2^eZU+nUX-s7P2)->Q z9HLqse*wD^LF0ut4&oCr2&LSRb|@me3#3WTXH+(O+-`@n%pEEzseqHoR5q)%E9rKZ z6=i94UXQ!NWAh5^b4>WX9 z&btNA(|F3~H)1b^d^waaX>udJVOROGUOC4$V%G(k-(!;HFEGmg1I}dQEHm|Klf}I6 z0!RClHPKw;V=NBQXx8|4HTBdvyOTn%DfA8ORtCqNEYf$9r(l*bEBPKH@uc#?i|@H; z=hjxf0M*Z@{+^bFY^z_SI*I`dAy8rWWIPFw4tNk1E4=}B(c^(Vj&Gn#=W?BD0l$z zJFZI{j!Q_ua*T{Pu%dZ)J3SufZU{{t=apB2>^b)Q=w}}p<=}lIUZje?VFtBf{c+K| zg=K9L?}BCLN@H02(H{P+CqxG@gYMk!3hfhIqau0)G-t~4!Sqn;C5BR?7-7I& zL)U26m>5_26%?scZ9%Jm%I&;UG2dK_D}U19uZ2Ob(&nCvx00I-$nt#oxIAGxG4wde z?kVn)D%Jd|9uaRP1M!I4^G3oY`%4jow@1cXpwbw48Pdcc8 z!Z%Su_9&xkKhvHMxtbrVJpj56VxOPfYNL#+lAhY7BWS8LwzL@LD zSE+=>61Bh<>iZoe|aj6Et8K6c1`M`K4M(2k1Uwt_{AEe(jplf-TWWPoA}1Zw^c{IsPutJMa7fWL&_ zRpxM^cm4iSOQ<|x$sR{w{6V7ymi!B-~ z)-%Wbx~JOicU>7oZ{LjwH1plyN`guche=jlBEU0@4` z(G>CH#^Nm2uY@6=@_&uC^&H=AMr1*Svn9LMTaQ?EO{@qFp6(<+c}E$9Nd9hh@2o-N zv;4{Sh}Q%L<+FDLgFkdiAEAs>qga(l(N}yB$MTY!9YGsIP+Ep$qAI5OF7`YZmpq z-H4FhNf+?PQIs^#wPj38lyO#mMo`!mWi>8riSV=(@4h42!XFR~%r}I(slfXnnqr9T zLiZ+n3+c4d-cqP5%6w_(kvgHI z5oNC}8qfHc9a3WW=Q}5~I}S5cX1i%sRYJ|U?g@kL|L?$s_^$i~u^ZktwwFJwY1$NR zM0-rjnu4ZTrV-N)(>ps^TuD`e&+#B52x!>``VdLNCdC%MGJec$pk|M~uJ`Ckvj01bQDH##f2 zD0+MJf#_aDoy22JvF_N6*u2=6VrRrQ#4d_m8M`j_-PnDxCt}aVUW)CDy%+nWs-((Y z)ls#i>iVkJ<8|?Q@#_T>Lu0ZSMRKTGu4zjC-q=WNzLS%b84PQ zYiT_lPG6LM3q#<>zPH>Mi)A>=G{-)u%Z#)JtLa%r+Jt>h*BR*&Jae&;HpAv}m65i9 zFW+LM-FRj3Qin5=*H;$t!?pH8`p1GITAl=Xw|Br)6WY;O}uoaUAci5w)%c zDN@_xP(cYR)j#?AW$pQE)gJIn@f&>zFOBzm8U(N5DHFb=T7VsF=VRZ(qv5^kN7pxs zJ%Z;VwrvQg3}CmB8Q7P29`+jk5_Ska8M_>wglGgW8$?6>`0g(NIX?uMsRG(i2_%zB z#O*D?PP>aR2YL}paVetalF*gPuotV`#kd~cq2qwk1fbjkq$i@4?bsP&lGyopGCE-@ zyiZQd(JA;IdpdOR1gss{qaVAo{s0~EFnl);V#nT2kd6iWA0LT*Uhl_j-NtrePuI(^ zpYbwyabCo})I>0^U~lVH7|!#s2P}4;W~X6y*bBrC#V??n*Q0~KioJj@!fw^iVaL~t zF~CP*2iL2x1M&0N{dySwmp$y)*y;LZ>}q``NaJh_|3;9@X0hk%h1dc1N$3{4u`BGY znE%VM3+)KI0z1rp7dy}Hf=%@q?6bWBL)ynp*n#>3>`wg-?9T7O@A7+d^T@_kt4faE zxNZ%Nk_H?#to4}VxU!?~niyX-v|$AKCG-I9(H(x)@H)zqT#Mr#jWV<+179O^y9XYoP4nP1Ge@*Vsp{yqKxe}ey-Q?)oQSPx%*7k9G8I>|y^l>hb^vafEP$am>cC0LK81VH}#cOIPEi z@V~9#j%RcW-ra&d`EL-VEm+mab;SyR_S{f}V;bc&P!Z{|{N!N}E_5CA|T{o(NZz#0t}THVy%%mx5}>iOaR03Ozm z{@cgQ!0AWp|DXT=&p$*nYY$TZ0R6{5JuLvBPZQcuFUQ=(!1$-5i9bBH|A0MKrE30T z{IN~^)Cqq;3K4b|M5@%$CsU-wr+U2ylSoOjDC1XKYGxWAH4B&Lc7`+xc{^Z zbn&Aj{0|@(2*$PsHYNaoOX`ok_DBDrlM?J|Z|CF;0Jus2xOWTypuUK`@+{grn*3-H z{eN^n;{xc^tlj1U0R5DJe;D2AAbJSgj{pRalOGFUT|64@f4Sv)!?)_!Cs-y(;|~}7 zKkk3Y9mq)ESYJQ;{hAI0cT z1EPL}x$v+6Pyq0MdIQbghOmI7fPk{URDl5jy*Pr*LyYdsmW+e}V6d>)AegA-2Ht#r zd}_Ib*LI>ut>0Tr3=G$%-Zecv3|OSYFL7mf;#3R_Ab`#sM6Un&6t!fOh^K)Hw&OEZ z#hBkz)eyB@Yh}3Jp6>37<)!Pf+nl1sm*$iQ`cVfYkWp|!KtXc`GW_a3|2tM$ify3)*t<(<&B zrX_miS@09wXN}qAgaGvtmWzk|RQpD|W2>2Y@cJFmQ1sMJ-mUX*tzPez^Ads&jz_Ki zm*JPcgC|aXC&4K`gU_-EMGTR~ujAh*jId-Q0p>&K74TYp$1~5kYD{uVq%6$c@gV)z zo-UrmHAJTY21~4LS_dMgx-S@JRDzy%7}@-~EK^n!Zisa+HjdmhGTyesqxLQwXv(~~0_Yz{B{ zj`5~AkN41hjC z*L=DGHy)cDi8oUqvdh!B5-rCK5cYs1bNqE@Sk;e?I%=Lc3VFp|0O=j-k^cD+@#!A@ z3dA(&F|i+K;%gGh@eQjnrCZ9tZtu&UKDj>OcFM8j-TTT(n$8~Efw5c!A^3tHGKUdV zgfabW8(auoz|$76-HhL;8};0*)|9)FIo9?o?l^n_&V-=kpDpfLWsQQ!{2_1~WZuY| z9cIbAIr>nVko(g<;|A~h63_2uXHaSv*rCx!NQFl2p~q`^b8-?CKVZTwm?yIn>K4sj zJ1=mDBjJN{jged5?lB#I)w{Kv6M_bFJLPjqJ?JBITus&l4p^EF&ad^VMDX`__qsNF z8EnDm=Pj+(np4WTXU0*~{IIVPUY?<^?I2=`tn(Nnax09qL(04Od#o?+gCeC#&+1d2 zQnvcoUFEzb+@9wzTrABn+6en9&i_@HpP!3*7H%27Urs&IdX?#YIDEnSHStaBT{M57eN2Ane0B<^ z=z7?ldgOp%t0)jyrA~Nj!q#)s)U%z}g00swzdQXyLdEXoXf4X!Q>jA=R;juU3|~UY zDu8h*Tgm}@?1EXt>dmYPtkeVL>4thi)<^l^GxA69*aG&BTJHJlS#(X%(f#n%^WMqe zfVbn+z3y|B?J(S0wGnO0i&$Y-jz{yy1bAv(J#e2xE(>RhenFL}*}FdmTP0>F zGfzYBNG^|-hWC_O^rmRB-&c*@ywxcZ4$Ee-LK9b)}>~KeJgyL9P?q9aG(;e1& zTE=N$zxosPFU9!1q+H=c7&PQQAK;fPd3?l4<~?@_*lCFaIEfCjzm`m;q#4+XR3tH; z(B`B$;1TAgAUxKEDJ~6l5w=YXJ#3*Xy^!lzrW6GihLN4pbvYJKjDtX?zek}d)Q@1l zZ>oXa^#nRf_<5BfF;IuZpbyIpTk_56X(HMscTtKQ#74a}5E(*T^CJ5kP z|BJ7cs>`nH((w4@t!hcA#r|m{-OIStvTna{=hvKqc_ow^j!s)y=A8JrWeMcO5JY8XqNd4b@i zG*3#SLaOAlwCTl$b6wnr4Q zy5UKigM7Kcz$p2hO5>2MS$U}6dBfa4>L1tea%9vZv^0v8v)&DH=uUzGk*2Y$u*j5O z!P_Ta)*nceyzyHPAnoiV6U2kBu#Hq;ba*1A8z6#`*M{`Bg%}Qt-G*k-t!3EV;MZJr zhWkeTMfdukj0hq$7X!j}MRZ02ogwF*gTtkz&V`14{KN2gN=NWs`1fyP;knFzrjqUg|#}J1F*auq$L6;uUzx zSpf=pJZ?jr*qKBwp0T0zO0Zh-*|}jmgG_<0Db@y#LWt7ZT*Q%@GzC$#gCk_SFR}Tc zB08un<%;|{Vxb~i1+>Ktfmqmkj7bNM_)P_H5W{G#z&okFlMpk3oWc7Y>v@~*Yc)_J z6^W>X@S2HO6wZ(ikiP{QIDVIHUA#)J4gVEoSY(7Lx>i+=96sXV#Vad`!Ht_lA1#i}fzfx(Zij6sm>6>2 zHo+faEt;tOX7dqa!%Ns^8C+}04=klbCX(!?tZ3+=M&?6% zT~?TL=#vEduOaE;Hl`>xHF<`A|Jqv%SD7NRkz~J{slci4w~O(In<*%n{X`ckMZjCS zfJr9Btwu0HX*EP}OcAL!EderbvP<8vW5iAEN61ZWCb>85xA2Sl6e4dj9GKK;53ge4 zlPRR;oT`(Qgkg}GtLiQZ4#BLCcO@X*UV`0(h2pi>wa;$C`Sa(#|&K+DhQRE}|rz=(Y=f z1vsHQ)(l!n3I=Ix4B)J7Gr0j1BnOHzSt(mXEX!%QO;jnVKKHFu5I6LJy_W~$Kx15{ zTJB?!$fvX^3gP^gLehm!J9eTr;_WjyHJ$O`C!-Y*nsS-mRvy*QyHXsv#yUOvwu-<~ z5L1OzZe1=+yTYUTCEYHv9;Ke8^ge*47-P~5Xr+vMD=O)%3h1?LMy^R3>? zLz5wKM~XPVY^#kOoa(GXM^%mUs#@6Mw?}v#?5#*!n2|T6*5fapdyHQT;yy;jzi4Up zcLv38EJ@uHnpjt_vqtX_NM1|gKBQEKdYj{RhQ)7i(5xO(Y`Qv{Fy924KjujZg36-b zyiGU>7NShPyDM?N0D)jYl0SRSo2_1ZVoF025+l6>@S!YRSX|gqFdVFvtR@C?DQ0H# zfYfz3b=2R%D`~BS+$iL5Y!a#rG2<2pF(6?e<3Ql1;I@Q0%N7VhAdrEtTmhs(VIsv1r)iFpahMZE}^o4V_*P$lIS7+x34AyB_)itfCd5CAO7C{(jrucrQ=VXh@DuN z>gj1nxFrDr5r6tNBYdC!{>AT+x@8oJ;@<)fDD}j${a=g*?FB*IWB>qq8Wiw+r~bF= z^@{mf8vn1f1q7gt#YlvB6!2oelm?fAo1YPtEU2noXOXcfW^+p~$phPGAen&D>f5rX z%!E$q&$=P)h=R^vIwALr>N0{b8|Jwc|3LpH%OoY!kZ@4Uilm6K;NNHAqm#3mKWd=B zlWd-Qe<(1~3{Y9Ow5e)*(!!`(Uee20AHB?UwP>&YoFBb@aDi{n&RCheKzAu=r`g=p zm8xlGU*x*dd7gi7{e1f3_iNx=);qs`{sj&dG$HV)Z_mK0j*kmV&pg0X*H_78HNjb* zX3e@jK2;xf!A!wDHh&-3!DKhqe&6fOtvAAcU+}@{H|qD7e?UYuPAw7RQN(LjFA`O0 zP_S0v0*zwO=2rD1Fo921^?_KRjIc<@OVvPJ-A=ND%5$t2 z(E=q4p)+~nG3)(YC)yVJwZGd?``+er_IuTbwO?xYB;P^Kea$OE9wef;IVc?=%1{ri zIxvntLZ&}VEJVyM&&QCVI+TsHA&7P8=?Z-NE+KAMy*<`dkCHn`&YswZ)S);9u|!lU zn2939nShWv@MIoZLkRo~R-54SDc$itw6$ZZl|72>;Oa)W*DXW#z`Z+~-M)}_bj@u) zLkPQwv9a`lO@pqp)D`2bt394^Z70GWdnH>zBa`wV$1hIb5DEmiIC zE%L4JEljv1RN6^OOA<>$Q2cWIwi2FVn4(`1O+ORW`3Tru!G|6)H@^Th=+B|v&pa@Z zZe(3w+sZ2bf3rdh!3^iyk8BP}!jRq}_P2yKsck$dNA`x~RZ)ceVrlV$_!%c&A;i2I zR=Na}*D7x1RFkQjqpmISbE?|w(T9%!(i^nd;M1PzZK=D8H?=Ql8b6UdR4gEn!5q3R zFi)Ra9l9y7V29crx=Zr4kTafGr08hQ`t;vL^7FXa;jtu8Q9F}jS_Q7EuBQ9A5o?&H zTvqkh0=BtKW9^#hWu>c%7hHWW^*{Ip14P3Wf~m0gU7we&^hwPT&FQu;Kc1W|6+i2s zC+(-`r^cttr}w9tXhHbqf&!z){oMUx=7jp}`tFL%igcPQ%PY?7r}nV+<@Rf@JNF|` zUN5e9$=BSPdb_Uc{Oiu{!0q$v>+MTFTyd026lx4koGBDu0w^&Wk##Y4Q8_Wb;jw|e z+S^;EaLsV{d^1B&L#sUpCYd_LxwMOnjhwN>^29l%DkU7HXC*!bC}kK4$_NI_=(U2Y zQarA$-ePs2)s#|?-9J`_RTMKWj?V*@kt&c@% zGgIdKw84q=ZXmwQ5-fM1L=7-)oYf`1<7n_nVMut189@zvi`*$xOKaf7F-bV;#Ja2Y zi{`#_Tl{qwH;=2jtE(Kp>CH_OomB7Rtu$CDp$<>ZgO9w$|=wnO$3I z`>kC8L^v>CBTn9qU!J=j36wxto@jxFrlEKM^G9j}N7>iCk2vHx-0EpFK&Pi}b?HQ1n7_foauKTN=Y zc;)CZNs@~&%)=U6QSzYmKI<@-y6)uQ$U-j@Q-{1QLUtebVcuD62^*8gF(b@^hr{e~ zO{pFW_eNu0Kvo`T$DuAAqWpfgj2iyP>}@!P`x(qSHpZi9%3^C9uCvQ(Sk}GsXbXYV z_-M>uLQp!>f*UQYkbS1m0>Te;B2-#B`Uid_bSh5`99IWBI)mANyA&3Co(nHQP9!@eo*_u;<@THmQ| zZX(lsCI5+HX0V?k2h{o)U{=@G_p$;fiim;nA&L(i!jQ@l;TkjrRptZNL5i_KM{>LZ zGR;Yq^Tfe6WO{Zcqw4iSjM@)GjU4V9Z}f5F5Z zdkfih-A7&FSP`0Z_a1dK?ijx9L?Ij$_-S;xacW~rv~Y%n&X{pd_zJwyhACaA$oC2F zT@end!<2Gb=32IvsbZ7KDAfuWd->OziaIR*h7i#o8M@zyy(kx|Nlai36iwTmwF(w- zjF3(C(A@7hptVi+ahj6WR1#ju93qhn9%TBu;l8~NP^}MVCM3!L06dfhdIR0@`~mnv z4VXzCl#qIVurEAh*Ev&(J@xp^HH1e-%d{xJg?9oW&Jy|+HXon zID$Ne0xNPK)<;kLyR%$(^Qc77Vqk=nbVVQ9FH=$@${6jOl{Pc0&+r^ge+H6&W6GaE zuw!jBQXnsk3XK1a4j@A}Lry9j3410kB7+C5@b&l(dpxdHxID{ztIi<(ZP;7t9@!Pv zcWZA1<5yo4(!F-a`cb<$GA37C6O6VWv+oxZ=^twW3-FcV7T>|Zm%%5GkJ>fy?#$W9XehHUmpO?k;1d%uEEMsevxwiGMj(ki11~>m(S0m7`Mp*=u^ROzL!9PESwM&8K^l3S+0xX}LE3{6z&+fnx)$!V>fTA^O%BkavvxA4?qsDp zRKOk$?%yo|a^<0?aE0>>+;d&Gbs~pAHxh;q(57-}VTsQf7uA2WJN@@B3*!x6K8}d| z(ij6&SOKAcTjvjNS(;ca0%>md{uLt*=aYb>u5Mbq_=v1Vp<0Q-2-CCVrEEv>qq3 za|jH4m|&{@>4ICToe_tW7bbVAatV!`v4<2IB0;{i=lC89ELO`P^1Tak#VGrqZU1?8 z)6}uRJKu`QXU-$phM|wlLRrI*MZ6AY7Ovva(w&8;)UKUGD%G};?W-z3BW&7mQt$m^ z^UZnvw@mFL1ocYh6`=cuo3r~S&?jA@&ZzmJp>2(uMhu^w-&D{kQ*X`M+Be{ zY|aq3QirW5yjP-Z5?a3TW)+(i?Jup|wDX))%`vjh7FP@EBlP8QHh_lLo)(MRp z|JZctfyoWdWwTS{v7@>Um`G7?R&lj&9h>^HXiy{s?7;I-T{m&xlV~i-zS4YH4MTSeQmBjr7*>^I;K=^t%c@aZ_Y(PlNn<4B7EMQBqTLJqn zG_v@g8p5n=14jzAu`#UZrSuUq^>~!0o{tlMo@OYJ3XJH!Q#kN3mUw#0cYPSIS9cb9 zWjJky{`aEZFQ2>7xf(q>$B@Sn;EX@mPxPmxkgSNzTs$MjZr5XG9A;KUZR5-7<#$Yi z5IEHR>AJgD70OW!Le>$ZeCEF@@^=NiPZFipz&VjY%ErF7-2PdeM$A`z8!)#cIM-Os zExxpA)mT_Re&NMjU9Ir4%XQOO4&^e-b;s9VYyZh>V>%(+XzOc{rjH`hPV^lPe{};z z3T}JoUP&^JaXn$d2qF!l?Pald8-U?Y9&C|}3oX4MqHCZQxZ;>3bNqyC2eZbw60TKwb@>N_6hgzF&BBVa%Mwjy}VETo73KT zs)bi43!zspq{bk*1`M#0u=mYYijjh-0IE_fv|CVo5_5?r^t-3a`jgJ&Gf5S;-cSEf z<5}u;N>Fr0UO@nZnBkxUtWNnf8WE)>&KJbMw_?{$Iw@9+6{i?sohyDss+E295yLc-P*}{I^06??f{H{^rIIDnV0o^D5Mg8+% z$JBi8OR445QM8WpPNujFLu%2?!T~xj=y$Q7hmRN45hd%WiqD*f%EUUlt#$VPyKBld z$!^GoPAST-8pLhtZ4!wVePXa&l0^#gL587^FU&c>TgAEW)4UaJ`m@>hROBjEMl`UK zQyQs+a5&L^A!^#KXm8fIx1kbcDO8}soP5FK^Lf|n9NjOUd&_gZ9FHrH{15c`fo>}A zVMQi~5`Sl;QRb_c34&e6b@-G7ZVyJBnTZ&K92CrUwQsP$x4Jr(ri?eY-UEgmYGzYm z+|KU~9`ri==HU9>Xk4x2%$m8Qlq6IF$-&5&rgT$2yo@|=LOkAMc1!txwtaVY<+Q%} zzf6$Tje>FZF$rNL9}>HdVrN&WX{2 ziPMCwHhPjdXV3ZP>vS$PJ)7Ee)%^uSP@g@(8!s_~QtQy6MDfkFT!W!c364{Tc=am! z>^3Q*-#9%H;-&I0mk?V@7}TgI0?g{4 zAxYcm{=cLO{yRXYmiq$QzYr6I4OYkqMl-qov?C<2~o14v6 zr>Qg!W~V83Olp5x!Mlw=RV*m)NOau9%kUrFYG3~2)zWd_^JIjLelBR8af9qz^!Lk+ zZMNSGZ&DFy!LPWfLBPy`6Tk^NZdu5UtC$}$7yXU~(RRQn+o0raP!n~q30rxn`|qHp zXzW2ljGEQ1vetV#6lAH7jScFmyjsx`;*0{C?FX1MH&hES|I#$GO2g>>c&Y=Z3G>jZ z=};!CV8k0CXY>rP2ZY-K*VFbF2EB|=#2ikLSDeJmVfQ(-^31>sBogVdlR0`a2C_F$4w4$DIy2DJGzSXKnaN3J>*Vl7jNXfBq`r_d%x0!)1aN?3 z{02X^HS<8Z=A`QV$4oQNlYbr}B0U-+F~V@lzqINKc4(GkGswX}JVna*V!x!GVt}kD zO?YnY1kD@g(iCT)x?{*Dnn_ox`~KkOV-vJ}d8)s(^@-X$azJwf(*NZmc>!Xy`f5f+ z-UTJC9KE`cy)iRY*O2AE$KFsMQ3BT#-|Qqf2fy8nhx-0=`k$iS?PRc*(IZTT*|nU8 z{9cCXt@l8I@t>FlhGaWspzevxQQD9PoPnkAJ6bQ|3r5zyALRHF-I|Gnr&R&Ulv!@^ zjTk#$#YF@yP1DTQKd0DG$G?`M55V7DQYMI3NF0$Du4ZNe7FflKb|vZNppeI^2SVX^ zR7Er87S|_(^IBg4$zIpbAm@;qe5%!-;3gbhy^_$JE+gUtlLWCJ4$>Z@ZG_cD#pJes z6L15gI2Z-O>N3f-9*qYn=6acvxiNjw=REXH>dG;FG>X}=PWW9-uXd|UhIuQ9PtP#+ z3;Ok@1DDT|X>6P9pgv6CIpFVW4_wuamaV2#7FdNQYkn#9ZHnL*=SbE*xXkzpFTpHr zOCKgOOG1yMXq4$0xtMJ!AI_MrmKe$j`F;Ec)X|C!XYgyvZm^$=b@r9s-VU`K2(X@# zP(?|S>5>aYdt5vJ-T`gf*dpFmFBPp3kNV{93v5minSNtbn`0zUk?e~hr|HA&bci>3 z%$qpTU3Y4itoK0>KBnMSY~x5^71;V&t;XP+w*}CmFcg(pFEsh0r6j7By#NqrxxO@= za$?M+sUcpKJW|5R_iJJfkBUe(b|T-PPcQF&0kk3g*tnCK<}5U`?uG6*x0Bh?;==75 zM44QMXV9cCD9C4T-c}xedv{4|Nw1EYmX69PX=4KNkpaP7lJ(8MAfFlSL(ac3NB^+# zY{N^=RVEJ~vGKNEG6#=&^rSrQt5>-i);)`FcFlC|J8YV%MX&cM$Pv);b&fi%`gfGC zXN0CN*H0^Qbg<8Ob~DSzu_4UO#iYde^iwH#zfpbp5`}oUpF0+f{{*o5I|SwW8Ta9i zMF+JC5Tl#=AV5kO8ib$-*fiF6<#!eSLCv|##Z59ZG$4^VYLjprQU7AWya+0Q@ewI^ zw3_q1R6IDk4cD?w*W&JY=O;F>SYOpvV?QLd-dGMsfZhI$(Q5I6sqocvCH?{c5bwX) zy$CQ>TU8G^JVHg&oYDY*mfdT{9E!3Gb%zive|5i)IJsT>Ud6TRru)@jj@Tb>@eg-R zg$I%4L}Z_J8(eSUK&VE$g5vaT>KM5FOPCZ`qCn^4%Nss^mN=l!rUl#KG?xyePel;% zUavw=5M{1rQAm-I0!t`PZD3AREHSqaJ#N|)luFJ!3#=|y{8dz-h^-^bSuU?AD9##Q zX?;+P6n?~RWhQ5G*I*``?fRRqfxCSQ7a|2F(nHH2NUD#G8~{`Gu@$; zhi$9_?9YPf6!R!=>vt&gP(dZWhI3Al^@un`1_fPW@o2R9Zmd_SCb5YMjY=lHi!tF?Xm=g>= z3YzfRB*kSM*IM+`#VNsR-F<-2GWTyDS5MDtm&K;e=@{p$D?!M&q1kWXft zX>E&)ZX?4NXXC52fLi_M-SEUg99t^_yA_=5HL@ifvZ<-8oI`GG?!>tLYtsYq{%QX8 z9hwjCd$c+?6whtBp-zx;s=wJLhCRX_anPlbf)OD|NiLs$ZR!E5UYBPU4;#>!pI&uhv{pV(LmWhm?FGx2BIy!F;}u z(Y2Z~r}ZGj4|f#{KJsG`2!6g0;ZI^aFmtY=RiYp5!QO1-PdXF6_&5EV*K{Hg=>}De zK&dLsr46KJq1_-oqUC82md8J5MwAcJkHa39S<94BF0lY zzl+*~#*<~ZFxMRZM<6d+7p^w3W)Hx2#dSf~K;-8ZwQ-+$Q^W_Co`AETmNfScNTQkQ z>?6AHMA=cAoIiAKnLT&T!=Gy#?>Jz?by2$d>Bo7oyV%!@;%q3FqhKN5ueA;XQmdFR zOgKlhNnb$MHIe2Ea8#3G4T*lyU$7|Cwr*f6#(`7$&M0q>+-f+)wElp|+H0@RfE0wt zT~j(!-7NLN^12{6fj}v zq2Mv#33;n{f7$AoJ(Tu?{ob?X&rn9#ezVLZzQLcu+_=Sed4~;mKX~n0eO$aXN`IV# z_}A>fP^%w3uAkQK~ubK@8V zeoVr^AWeAbdtCNoSCP&zzJ5b&qiq?@hTAykGDg;sMVrUP0B{C;%f5a2Ol5tTN=|DR zGCecT{Q~fdfrZ|AX3UGC4t_ublJ@`NfMs@+UznXRGy8KzJctAL1 z8Ta)?*F*Hdd>tI(1us#^DY_@bE%%s5Q6CmPv?Rs6%PydQd;*na(9tqec%FdF> z8TCcCf5Wjzxgy}&_}&6GR%E(ujBb@Mu{E6kyVPyrkEBM>!ewQiHx-#;Y>#qli53S$ zxlq9=?=4kgN#)-eX)R3K7RI{SZ*u-RsONE8b*lx=6;Jn|EwTBv?Ni{>#ZUCs=o^2q zPlvZL;ck@}(>zoW!(PF=RUN$Aev;|M=F10kG~X`bVH(L)KNVaW@5wW3%ygJ>H*QT< znJumi3dnXttDEn-q5^LZ=x@~PH$a)$sF)BCE!Ha$F#ZIkhGmEw8%&1D@a2k1Gz zc^|~ITVWWYh?iK?$$4jJB>wrF^Ecg_WjRM81 zifhA4@=T8JzcP||_^iHNFrwVvW{u7-^8=pu0`x3Ie!;Bkt5I&ZlM!T~qNE?b8-|TN zwN%Dm<~?&ix+{(v0zMUudsf^QM$y|dg8Ah@C{jRiX+b%cAq3lBO5>y9|CRps(y zSK|mjK^Z;;>lP!#`-M>awjjju)qQ}{FC}&wfBay-OZGsk7mEI-*O}6qrVrD`uDqHl z4k?h4am8AKQFQ)5E)_U3=wCTw9) z>e%G~{P;1+1jGL>gsa=iXXc-KZit|tr;PkTfq%UU4bs)%ZY zE}T6lDF+qXj4r!iyy7UA@_Xa3sV$V;mgD}%-Y$>MJJ;S)Q|r-!)Xn=fJrn^ZQP_Ki zpcD()AshtxGfdXad#UV0c3X9#c>dwp<-F_S6N`?_(Mg#j+oP_9+A25PUJBFuGSd4x zt;Q33vY4>lAbPJlahxISQw>cG4G~q6_hf0Jc-7NPJ6*iB$IFsMyc}be1oj-&(u@o# z)Ne;ZZ4a(hbX>b^D-!Hm zo}Q5-EESNL>E?{3X6Pqg96yl;lkUmIX(`v`Rkb8z7qHj0+v$f`YSeoz14C=lNVjFI z9PjAirA5?}(uiB~ZR3W}ksQ(Kx2t-phZt3bFGJns@YK!IZ~($MIo&Rsh{2-9{4G}kMOijJ(~@G=-L?EjL&SoQuTtg^ajW>!VP!Jo^6c%8zZ^_8O*<4jwn&0Q&Nmk1+xt;C+jM-AuA(R{6lj}_! zU|-SG=^K8UdA6l*@D@7bRm0vr3bs_HsCa_m5!GteFJaF}e&fsp>WqOYJUWELJvVKRiqN$nyyd}`GiJqY+6O7EoG ziUn27bj@_q;phv3MP(aumuTeI_St-_`-VB0ANSo2JLthRCgPlSKX$KDh^fhx@o%@w z&f=)6rZ1!5Tj^#JA-nw&=lF{2zE6vQ_nF7}_oL?A9m+}iLV|hR;hFmTgqw=DbuJVj zoEx@=W#ngX&+s7~K@frgEo)v#5SIv67g#;>D)>M(%816*<@IbrI9O(>v_@ z5H_s$)-miCMbGzokfU_k+;PaafPYG;OLl1?k5|6HplcO{fG8NRs!sj~>D()Vz)Iy%EI79sU%r{d|RwGt0LSS@lbXoW8@#xDZ z_7isx?0N~iNpDDQS_1YRByRi+Z=lLP+~GaA!?@Fhx@ZpsJ}e*fi1b7VQugEy%#TTv zq%3J8mzlYnr|?nugmz6`oR+}vO8hX|1^iHJL8HU z5)l|2u+-n}TVG&h+}T3-MUE2irz^nXx8odZ9A)FHX~|=c^@i32;)hT=_~H=tQ^_=b zjMxK|%esq(WV@_bKdE zboVo^iDQGBB8T)k+w2mDKchsMB(;=?$xWX`QJPuvUCg)to}0Y2_v=`~11`LM&&oq* zF_>9Rd-W(qPr5PgLqd#hO0sADg-oT0&CcqXIdE$PzRn=SI@`RFT5Z{kpWXQJK66fTF9at$(<3a(n&bSn209fj z)84m>E5~$x^FqF&{78f$183kD&4Fhiz9l0iBn{MUEUu)|#i6y z`;N;7xNQL(?dy;&x+V;&Yv4lEP68&;F-cx=lxnO|8CGc!_m0H~o3H}CP^=EhyYvJh zBN(>Cr@yzS>RrSJZW**CoGsEO`3r8vJ;9Dx+#2hnaw3&;d}T&;J{myiK;$q?gHug|>R=A0} ze1v>;!*C$j_H;qlI&j05)9r>ZsEOu@1pHD5@;olY5=~TW0z}O47?F2umuu*;hWxIy z+|Q+Mc?b{XtQ8r3Gr>a47)+UAUl6pjIBt+r3tgHzA#-v}s{WSg3^z?4^Dn)iGn7qr zfQ8fTP*E_1jg4UD!%B$y%LrA=?t+{g-tOCGy_wNzWR*_P`+i-Z=I-^$Wu+Kb4(k{y zPhI&Ax;@t42x?GCD1}2{N{ibFcleD|Fv98T;{xQlVEEVuoGVD>4{^d$#}twh-Dmn$o`#_!&&FDZJ@c$iM@;TK%_Va0+z>;Lj5wG>uQTu02eOWT?M~I^U{hc zn2lYXN4oa@AeUi7v5vS*7Bj0=!2rH(m8@mdO7k77av10ng(WePa}$e&@g1}P9$9zV zD)dlACI3_zswL$LVk8nw6EwEPV?oy?QK^kqu3x$x_fy08?N0x?-Q3M{383NWCu*uD zf8l1@z^OJjt3hbJdih-J!#pHhqy(?I%1ot1;mef92T>3sUf9#X>S_k&u?um19u&d9 z=w28k6+?PFDC`NO?=_k3-Sqhvw@hFc`gakTEQpyoWvC(;K*!?NNGPOUF;;8IsvMWsX+Rs{lr}p#A%$OQCJ3`oXY2l(UCR$Hh zyX7~puY0~&_tL^1+aN$F*=RI^e> zxfNk>^t_mg4C|QZTR1UM^i?O6Fx zhk8fe!N&In21^BLjW(BpfE$CMXh{}N5)h~Wl*{8m6wqUjGDj4J0Y|Ck!P%MVvb!8+ zCt^)^e6$ma3zVhfk7Rh&EmTP94c&L_n7pGyI6ku`Cn_R0Vqj6(yC`B`wo|x#3WAfg}~T;u`U@kEghwz*O`zlnS`L z^*F8)5CMP^Se8`}tZ}2e6eBBaP`&Y@cMesSZ4ZMG4Ab(%hfl>$!?9m7e^Rmeg)?Us z;K|th%W+u^8?WQ^JaLiUXqN*-QjkxnNCyZS!BBzWY(hjz#1)Nu+2GIu2xC(U^K#E4A$Mj$;)s_Yna zy!G##Lv(NsV0Lg~55Nk%yJn$n@wV!@j+{EX(y?$L5c}u+g4l-|YipZ2m?x)%EN}#U zZJ>`30iE=1wb{_!hPK(V*|RL#82i!_(5t#}p*eB;2iEtS**ec9ZW17!(KfR}C5mSY z)r$kr=^fw$W3yI8}Y2uwz%eTe z@y^yAqkRMC?!EWiK;P(&);kyby1V=OIy(BckBn^JJ~)^foT%v>Nl)!;99&o!Z0t-; zAL$HF2H|Yb?eY6P?qKYn!X@S5aCu3X^BjEbKSpwh3+L1VDrrLzqDWQ})9H&Zee~Q` zT$t<1b7kY4=0Fbjeuf=UO{Q5F(xiaXEX!s-nL$3WP&#;|jlSh|uX``ev+jGl-<-B@_9PS_OE(cpdAq>{n*EY3<>gqSz=oDSmM(4le z%W#vCPHJ?67t&9`sROZeIK|gz4Iy+EE|I*e z;_qUqO(;D7#e_+4yq@ywY;cp|p=CmhcN|KcKD(e_ux?*ah< zmNYc)mj64*{n577=B^Xt!)LZ3a_=wmbyt*i`4Y(8#E`py$bEM(u(M?oa{q?Qm%#0J zBYs0XV7&%zN2I2rGfS3EMufH%ZkGbr!0nR6p;Uzc>L1(QGTYIfKLv zZ19l8aD#p7dmo0HPhWcR0KE4m=F&Ot&-pgi9~n+L2}`%L{=Jr`@3ixC3(J2!@+NTC zpYgpNEdOqLo;Z;&w1Y_UZGRD8$S_AiuGN&TUHHBdbyzH0qK-GVthRQa*glMWp)}X- z?ymB8xdJNR5?)@gZ&)qOCIs!I?K5R?#Q>H_g9-Y-)qfg zz~(_-0(YcQr@GuU+#w`z2U6*K`wz7O^qiWSxo;S;ha*tqFL>K`i@Km^7r=DS@H|wE zaO}~x5ZQn}ew~vW`%pt&ZBu*D6U@{6)9~@XyNdg6;P?;c=izMZQ;>Z;#L`-yuE7^= z-6z~T5pKAAwaw!Jf6rmor|wfzH>iDbVxQjnouV$iVoZA&-G2;z$I|!zfe>K%)ATfZuM_F6x__>p z?*5lI&lAxVJXDSH%`@Nb>GoH4x&yOFcE1~YduHKHKq_~2sAPAm%|l6$t-FW#-tqhe zd&iUSxnl2RT|sz-WWxJs|5}u9o%`dYNyRb{@y#ZiX z@5l_m?#>7fM@{wa&YW5{90Q46S6?4#jt>Wc%M+Jh1Reb(-U9v*B~Z=$41)JNHL+76 z`pLpeL&YjB{tbKv9_Dh-vZxRDqCknTl0@CE6nwI-n5*N~W8u&S%J$OI%F5E>%F3d@ zs>)v(2*AUKXJ*#cW@Zk@{-!=qU0YinsMl->_rp1&C+?_&P;Sh$tnNk$a64{_Cld^F z8$GrubB`VQ*S(WJnT&m7k^su_;3PasEM)W<3WQ$?DPa6{9-$xtUBhif1y~_?f0e|| z&1`C>cyf!}sgGD3nQ@yldl>hYdQ8xmB_#qUWA8e#x3R<}%n7@u`e!DaJAO7E+2yFr zPm2sU*4B^Oiv+$Ni`m%T4UZ5XIitA=jvrz4pc4g2x0X1{C}{U7t;CNsD@lSVyr7y~ zOVkM`uU#!}3+N+0JPSElkI)t{qM7_94ntQt3teg+nAtWlCr&_Rh0i%vUti&Iy1f-J zLCc2&+je$;Me*??1tL}WHF%SpI3i6g-vKv|h-DF(eFkpd{t;l|WfvsW7!6`w|!s9C9j z%;@N@@sTBACk>Vrdp(8jpgfWW?jPuUuXHp;dwQBJwgOweEiFC!C*$)a9Dmvvj1RCe z$R>6&`%D%`pXws8@$>4z1TNihe8F`~=;M$W}J(#YnRl z2r4z!q%HZUj$#9mZfTT6wi#0EgIZ9CFQSdlCf_^p7LrNM#o}h<_`UwSruBgMZ?*4+EnT_cDVttyF zG%?_e$nj1j&(jOj%WRGcaE@+CwV8Ui&+aIxswycdDTPM{8_d2OLs_V*=&Yi{S^FlN zCA*!?7KiO6_zZm@Nq2>NZE5|iBK)^dIDKGEMd53)=jy5hb#(zm;T(s@V80e4MF1j! zNW_K2;RH6J@W@6K9!tXEF*r#dVq-g;z~M#cS4_j zS1q@Qn&&E3HE^}65)r8+u54}sZNIBG))7x$vl6PNQ{q_P#eKw^uNUpV936Z$wBG~I ziJkz*`+2#W@O~x7`!RT9TwgKvY#i;YRuxtmaGnmW<9}3Z<3w68M?AAR1EpP?NJC^x z(h%$3_N!lybSG#7o3FPp7z#*Fv~vsEu!+rf#G}dqA!XuKhy(mri37ac(H!kY>X4tG zZ%a?h`pNeB5~K|b#s?UTh)bpuw84ohUWPb)l*U1m5H$5lg+>&T3sEEs>-YxA0^*yi zkcArMRp>%mu`x~1@*dap#pqkFUcAs>Py(SySd4UMm4T1$j4-B0s{Zj z1O&dU_YSl1Ea1LuBL)#1#s=Gc@rovq$e+t3Zq%k3?$GP}i`nblTzOpjucUMdXuBBvn;j1lHmwsW!Q64OIw28Tr?fPwFMsf8BRaOqsA1@3g}R0 zqW0AyOEj(-*{jYpr9pLBQJ`lw zPI~{hHou?eS0vhh-{$stehSa`v-}_M{Ex)*e-Y0QCf1i~8@U+Yq$J<}$u+-!m z507pnv$7@s@ba+kN2EFm}ZRGoGEqhrmfkPm}53*B<=>vq7 zImF7KF?y<2mJoV`tYKMrDbP z2rMT*W{j>VqLatWb-y=OHW(9k-F5#3dLb6vv9^Zqx%@Wvo<{y2jL)5L$irHi6 zYQQ1wT;fvW>V9eN`sMpInQG(#dO8i&ku|k&s|%HIvJuJ+;k7h^^38Sdh=)&9k`J z!N7nr?hOZkX1Z1J&n;Qy#oqcmO7_zu4`OsK6~LSZ*Po5>g!8g11;W$ z#c12kfY|>PHpGOI(B;##jrI|Njmc?sOsW`sGL8k66`3l$b085wA5AiqU)f8|M_8<}78N>_jlN38Q zbqjXxwGCta-A%1gcwucVb|>%PQVZ{(+Glk=zd*hVeFCmv!hY-6s(=3rYW$Kpawjaq zIrjD(bcX;yCgXevf#JC0B)iEz2XgFXI2U^v_jU<%!#C+qQ7;m;rfF8h0)-TH)8+$q z!>U*nbVcHgSiM1p;EK*njZRLCPTg5_m$)d=H~>#ATcep* zK_+*{+7X#-)t1I@ItDddg5EE#j!bCw8VhFhsaQMLFS8FD*YOvSj%ce+Jxz%M6-0_o z06?aIhU)4W)il9e{*Gtb6fx-A2&BwJMO6WF1 zVf$5)IXY5AX;}x_z#^n{P7jopQyy>mFRWF3ew5ztbvSfo+J{I$6QyN zxJAEMAQdFc`OR!G3tJ%(CQp&&-(9$2;y^gOi;Zc0D--`W(33R0pXRKsUHW-=I?_1i z7dze_Eb;mIpsKO&_k)dol~~!DO#!xMC8erT2#1=5fVC=(W@i;ruyxw~Rd#!o-`?r1 z_Oid}T6fRN*|BYRFL%2lRkQc(oVjn-Psrx>EJoIzwF=KNl{65kDioL*tq=&SjdWsz z*zmM}_P&{&_smvBT;0oeZyP(i(&OfB1uhT4d7+qi$jmcFSa)J$!(2qf3Aq!lbXBC$F)!wWzKmJ-wr@sI|2*Z;yX*_efrOcp_Y$H?n&PZ6;V> zKFIsB^BF4uS?`O)o!2kNd5RV7&We{}U#`q$-I$D*5h&}6k>AR(s`t!+U#ypL))ad< z$N<-53#>OxkIX0#6e8FKjx1g5Ka;xz8M-V-0enFNdeMJnuPT>zkMO?qzzpk)LV8qA zDT@HX0~}|Fv?!=}-d%r8h%l+;3WUL(qWX_ucp8zC#IC1ccvzT;y#nHsV|#=b_Pp|2 z?w6n!qWoPgs?5`@Dd<@J^-0fJf6Cr58P6kZvY*xBGg?(my1ntKr$CIoGAbDNyiyWh zvyXs{{^~E=1L3Rhfkb=PzX!rs+XExy=kO#%U!9g`+ect1_CbiwBR`+0Bg22j+xQtx zf!1rc@yS>Rv@XVZ{htdN&~~jljr4n$%ArlI)6MP;881J<>J)!KUhKG9y${Hw!^-M* z(=T3XTwEjwvvyXXjE9e>SK$oj*BN_q=^sI1-_Kq8N0N!${iT0YxqJ18S{|N-GxXEM z188HYu+Qh>&rh@G_Yn`aLwG9)50)pNW#!k;CVh#siP_3X7Lwag&0EYFHp^waEc zQeeQt35nM~&FbGr40ux}akg=gA+?1oPd9MdVTt4$*T%ZIKd~y32`|xnA`v6eJ!6nX zLaSB&N^s++7wkRHKHKvW9P1hz>poBOJ3srm4v=2ov3!{u)1b^ zZR1P$R984{nSX#6~~cN#bMu2{?jdO$m-Q`^57q;B)dB_FO{V zQldP5eS%RTLz(Joa^iTsuCwGKeB$`T1Y(II_TivkB*nxU2;lWSJ!t48@#} z;r{omn#5fdx#KxSb!#LE5i>3qI_dRWZVuGum1G2Ks(YP|YQs@O%|va@>Qp$eGZuQ! zvG*)r0Gc-9ew$g*w^zDq3f;v%Pg{vIIvHx(9T|k5RKz@o_&L#a`AK@7?k4%fNd}_5 znIO?z5Ts%d@f0G818gtRK1HU`OV}VtT&jplnG<9y59`@3ORnl8jJsBMuDcUNXM)`2 zl~)K>yV0SbEK~Cl79t@#y)$k3#I^@_Wp-vP-mDx+jaEd&dtx7r1|vQ5EKZtj| zDvJ)Vm@<>PvdtGX#Qx@~#{M)2izD%P-6eFAM%g@;k!Z9jGu@~Y*{s@fs0fs@9vJi@ z>C0pNf?hFMQOIgDyA^w}*Iis@FRLs^fB2C&#CY~dc7lj|g$V+^ktB)oW1$9bS(&#X z)Dv!L3H62=y``ldo{ENUZtm@EZtm|Z@weKB1A$>%n}0MjbJX8%8?LIt)JSILKx<`X zTdTjamBei)K1NXn7ec$K0uU*H1attcr}{!5^=|e;Mp=rAD+DA7rsBTZKpHJ`SQ;G` zoC9m5QrN=tm^K%i9kw}@XDp$1PnvL>v18rcSi3A)y0bevxX-bwAoDr4v+md`v#V`5 zYAg@>)0vyG++wTVP*fkNYImJTIc6Gfa5;+dimWNj)mW%^;59f$Su9=^+7@LJH?A`S z2-FDdV?g6qxzAt&iYN?vkQ9T2Cjc!LbX?I_q8q~N!H!IaDbwvVWfSR_? zH;i3zCP!KEc%2bD-FxZ1jcu8$Z>)Sh=9 zG@opQe}svK@tf=Y<-48E-p+TnwRJV$da`!^NUiE$@AtwY>w^!9v=adph(Oo0Al4!f zgq?8MH3g1NC(p4Cl*{}?@E6u9;kD9GZlcyAC$Ily>Hh@~|Kj&d;YlLR=XID}CKQ@$ z&c#BiK0#e%>V@@$Ww2(#7k2*nZ2RG!{kI&4P_?7Ze#adx`@6etY&Z@rRrXN9S-44A zJyCaPhp)O|ut({%%^#~{rwgAyzqieX_uUAgt86~1@V=W5Wa!smN0@a~l9Pc}rAmY! z`3Yf_su(wGahkpCyi}dzuQG8JAaTOA`(p zFy@sMrskv&nuu*{AengQO;kNbfr@9@@j?f2!33VS)efkVhfGsE9=aNwIEX>KRpG;@ zXJfx-L2q9l`~7U}BJ4eM=Fq>uQyBU-HrG+wQB%X|C}A@H4>EkCz~|7zgL+q$j6w{) zrpEDQX%QwV(l9|J!gAcH9f-qMI<&*LR=@wI_p?7}zVE)-`|ppp_cQn?v|N?HlU4q% z8qUOyLd!g_(?ELRWAGd?GG0JL`9_3Gf+Cz>InjRnc>D2V9mkJ%9P4F2+K;inXW3BV6&CXsQBIbRl$vL&zk zh5+=$J{o{5_~`V<_kDckQ39lim3>w#tIDJ#NpXkauIku}kX0S~7|oo1?9rKz?^9(b zH(dTb{FG*}@&+6iRlY|Q=p(!zsvL^Ht&hw`DMt8hZj3 z|9EzO;g0Ed#~$nO8V`WAII4B=L_-RQ= zAtWU|g-KXg`$oQR;wkYP zgRL78gM|7K3RHlTL^c==vhgHCd5TWYeQu&8ZG}h#3=*soqfRkSu`$!>4Z0O#G@`Q8 zs4&rU54I!{>EwU6DHd*^x~ihg=XE+W`Cmqg%t&+4f(W1E(ri`1%ijbtEC#a%FC%>q>-~4>)8Iwu%G-t=7{y5EBi|v3n zT_#Ve=aYW;1Nl3;g5I>q6GV?E$;(gh1Ej#@44STxqYw{$Qvvpl-<7O;gf=pYa zq$>dyi%`6mio)pb`E*Sow;5VJRW zU3K>8l-DycQrhYDbe7i5H8t+8E${NO;4HPfn^;JKxemX*#$FI|x@robFzj-L3c3US zPL*a_FFy>wq<Y|{(NUfLtm0AxTMA)~ZUm0u<dDde&Cb;#K$s-#~Pp`9cEcU>v5Q^WW( z_;G17GORkZ6!JdxZ%6;_feVaHViy+RrPx#O2W&s#Yom~SO4Yy_tpXJV0}zo8*mz2g zr>4}+o`Y_B)$kQ$9suKpCW0oIw#qMG&#z?p4mF?1aw40r>Ex2p;-o?ZGqf|ZRs^|b zLl%n_l<7ftrFiSdi!MsN*-q}FRP#~Ff6|}awwKDbijKfG_o-8^CQnsQRm!RCvSMX= zB)6%?S^s_;*M0}TjF5F9>CIY{G=p{ii6`cSOKmbvu1A3r3AQB}=lF(^%(tS`P z$g((R08uO#he~_}`AiQb`YU|FlAzO`?=A4Ew-wnQOqbGAbQZ2!6RzgW<|@zWezb{6 zo7l5wVq$4&0Ow8yma7d-<~4>!y2|sbm8YvX zWQNT{_L@x(-Y~wrJg%iZRaYOvckCF%5lkM`F&t2QJcdbpjJRRv113AvRJG_$3+x=j(q>y{L+)dYU}P$x(rbv#Tfsea%blWq2#AG{Qgm9t1f^rQ4F zF`z|-9#r?Uv{$_mwV4MsnFWTxB3@ue7s;%6m3Mc+p~#mhET%0=;SfuNN0LIuqy2J{9h&@4oOke&q8|N>ws##GJ)952tu4?nMLAI7jOi=7)m`sUSVK(;X&W8}ZuC`zI zoExC}OMe(V-;)d{`bZLi0!Fdqa8d>*C4|!;8~{p&Ev5_{T*)m&tPDa>tnmHV8`BbS zeOXF_Mfg$#4t2#I3`<0nLkq3i_yPdoo|H+S0ibvDvDEJ3<0~*pb%YT(uN5lTI?$la z#0A1T@i1Q#5eD~pbxlyDYXb(r&Qo%RbCr|vy8%G8HQ@%Y;x(i69q1aux0nVC^tl?g zJm6sMR|Hr{TaZJs2Z#pu?-|@CQXVZa07bxYghkS4N-+vo0$&qK0v;EKfa^r-13Mr$ z;i~RE-xMouPV|L7q{{6drmRzh@Ht(hOF{}Uu`{2ZK(#uHiLH_}nd+92yYN@n{rnxD zZBlnmb1d3?UOQvGOi$5w^0Tt;8Cj%+vNrWpig*>6j=cb0D2cuBwzt1khDn%=z3a2= zA7O7VWHPsA`Y17zJYpv$q>9v$R?SRd})6-G}#snFc zQyCu3Fif#8L&ls5G7K4`HXD#*w;#E6|GxRTnO!@l$G2}A8|d%r?dfiBYpAad*z7iY zc9v0({9P8OnkZ1xr1xo5s6h>>R|(6=p}H~UWqd39W@#zNvSj`PpUblPKv9BW{I;SJ z-pLCyANH7X@>V3?q!WK6H?ClvM7Au$)8h~CYi!*2@c6_-`2A3euQflb&0pbf&$6`x z$+D+5PUn}#&ZU>;?`pKcxAxJ3oOHLvbm{B+1XE?9H6NDW3_7K8;#+Si>nlo0De5bG zivYW29lUw`Llzs|4+dS1=hC+es}uw5r+F~QTF!6tRruQTvpU>`;}{8%lisa zGIcePNGRP}&|8)lu%^;&+dz>kpOF<9@RwWsmH}uh=qb$&<|*06-1NNa+|sUxbzK2T zg7fD=lmgu}tI=W1EGjc*x9HO)M}ubP@fDHKr@73@8cnY?Qo-N=i8z6+hXIvitR^}l z=%!QjR4@XnKvq#rCCN)f6uIHB7XMC*LT=Dv_yvi;>#nAN9;@of%Jo#O;1_3r7z8k^ zTxS!i5r{%WP{fsM*X1>k3AHyh0_kjT>}~1|2XXrp7v@;vZciGZAysobk#I(sxHFI( z2HKh&M^}rn;Jw0O;SV}j>g#Xn9bd^hWp%kM-+K1wGYdDo|5)EKx2xj5?T`KF=39RH z_&>cezP+Z)gYFD71H=BR;i~?&?gQan4b8KW!MZ(9`$FNe2hKIUX6}a5o!RDIOWyRm zZawnmZ6E#X$puF^-`Pi*bI9PL#ly_Jh)o$g35QWjCi zwUB*0Oz&ohG#4e8I$*7#tQh@bh_ed!(ni&A7nvqPn9){lW-cq8ZfD0kJ@@6K+Q}uB zjyV{InLd27|74)&V6^!_kMPPX{*HEf_odrkeDTG~UG43={IPF;vXn8kgc8T)pVCzN z90`+F^4Vuw>ZpDU}N|8Y>q64>bHrD#ZYBuwP-54q)R)SRdz^6 z*l$(1Bg<5`Hs7_UHD5Ta?`#TIHaB(cYw8{vXg#v$#^Z3a(ikocb~d%`ZHRWaw#J?s zZ1iLs>Q8I4;LdjThG5VD$jub~~LIHn8d8yCkNcJFx_y`R<+}H3T#)oqAum!6l@X9^U zGvUO&_kL~vfv?@O`qr6UZ@uxRx6aJG4Z2rv+;{u!`)=F|yA>88sr9wXY0AjM3yb#; z4&J}G@bJh;{l1~0)tZ{sp`m^Czo_pRY-fM<);xD{djR)?$i`w&xJci{$0A7T$schv zRGnV~L%xbZT1!PEQ>w^oI3ywtiAb1s+OSOYgpHW8oT3g%3O|w?)g!s8$!o+W^pbM& zztl#wX>)aY^7`xJN_DGKQ(s#XDh?J0nY_20$$Ra|laqFxljAk5dtu6Ua`$A}%y1Jg z*T6sD@x^^Neeu+(&+ps!g*z5sKeglaOG|IqG4+PJpSHKyD=P9PbIO;_dA;Wbht5w; zpXu*EGd*>FXtZXjvuiFGoa^dbsu_oTe}$t|kPEDl>ddsU$BJ#Lta6b2tC+^{l|r&e z4*FQjNTsT(4G4uvok%HLo&qsYu7%bcCK*}Q&l0^sKa4iNCJeKJk~Jd=x1C`wRg>8D zOblD%McY%ZY-qpb4mfrizIOiBn_>9Yk(+OUbFcIX&%%9gn4KN_z{|HBx%K9kWAitt zIMfeE;9;uzqNwA$r}9;#Gh1D0#sN29<>>v(p@oH^;e~}^e;~j;QydwJMh6F@(V;G1 zagoN{F7}ilA@>wf^s9($ zWVv=tMzbX*lEnYNIEGEe9)+7Oy%#?Tkljq4hj)yOc>kUNxHg5_LI=-VN7LH<(^EhD z*{PqxZ@>KI1D8HT2Z?Hv#@f4wwPz-E(Hd^6lN?A&211lsgPtmi6P4Ot9jb=SY>c&k z%)|h7HAz9FRGrPp&+ujJ-zn(;m=3Ua#(s6+`R7$z>C5{C;4;;865twmdWO9RKt_3L z+!BbJ<8s47r=a#o^HdIh%ZEK>%04M$gIx@11a%Qlf6&{{*WI=u{6s8+`8 z7wp=3S5Na|-G`|&b{kCPNQd;b2k)+~JpQMp$*O^Z&$Mq#4Nf=J50uCsTv&)b>8sh( zeK=5H|7oN)BmGligv94!PAI1zA&-$alac7)Up=`xKh@Es2;%GaQ7Sw(NF{nN5J?;$ zf=ESyvL2z%;sn2GLhyq1U7G0DB@W$x-_hH;I>W)@LLaVUcbX7)lcf@B?m`u#7HIUD z<;$|_h0n|Ti-qt}wu5z$=XqEytjOwf1qb`XC+rZXum%DVmAIp#BaLEvs-39SQ*E`V zH3ilRe&b1xdLXNyz?fT7UX;~TlVUB;&nmSWs@pp9v+J9~8TN6TDL=CyO~}sA*DINl zD9fT=%(aR}yxqlVr0=DRnkOrjG5$484)|WmqyyI$v2@ zN>y#plv`ipRUDplQ(i%eXmRDaJuEO^{;JuYR*_j@%_vS0ykfq~VX*jeGIG-j+B)rh zuRY$yX4tEjQw@qWO&^Np2No;!)r|q9J)%JYLy^Tl?WcwLfVY?Ch5V1c_bi=y5|xh#@Dx09U3JZopfY>*y&sKNB^2LFn|NJ?RP)+i z3Y;8JKzg7K4qR7-!6a*ADR3DZ7gwIvB)QtHdld{Tk0J&SzxlqY>HFS7|M2AWv;$$Nnp@FB)K6T&31Z(Tb9Xohi`)E5g(I1mkQpwwLiULKs(qU1*@Bys)J4SHd`M5LtQ!DLF?&1klX|WA`p?iSU^xfy_ipB zuFnO67jx25^g3D8+>r7vtEzs*sS3lmZC)CMcNA9od^yFr`dvDAV@Z*}#AC7N>1J4h z?#Rr|^k%zTyzINg?23Acx-aJ70{tKnh=;>sOX5{r6Aq7E3_ZmD7>tYk6iu&F_1joM!HTB1yPlD z=W2~u8Rm{#W`CqTnvz`z&5s|d=}dJPY-z?_ye})4P4HG?A~~dqw^MBe3WAw^&;bS& z2&c)$fObwB_W!O3+xCzA`GTsqu=KucNAtzz%K1Lehwv!;+^{R1E>) zW~I22fsE!SjOrzaO1iVv2#e6R=eE#}2!>MpMSHz3kSAGq^hCmOAuPcM;oa9`tB)Gx zeprq@4)5-a*S!gT0`I*3x(~($(M!m)n|W0}i;PgCi) zHkJ8dO>8Yhzg2iCP<;7vf=1xo-z@f9Vc-8jr`7ubbXvV1p#DJHj|A?QX&a^!xLl^~ zm`>nlnRaOTw27xXc{+jbXg5zMa2D;sbOP_lv=`F}93j&_OeglMO#3mN*fTO6z;t5W z$#fXgiMf~Q2-!%xDpYGDVs81%Gf&!rW@*&>C&|>C`>$kU|3msQ z_5OJ>*qgxn;bZh;YyS)V{l8y(|9^Y`O1HsD`W#BUR2g9kh2)K4+DwZ!H^&&8f4xAr z9X=diW0z~m4==xGlaB-MhBap8YiyiVVf|s{|L&UQ5#;3Z@4k-m6KiW5@YcMFM_Bqr z`aH*Xx$L)|^yHm1Qju?z#upTA1GIM(= zGt+sQnfLAcjb`dXdg^2o%l>83{%v(MRZmrwRnb6+*0riN?SWp26tOQbT1JoRkx>KZ z%{|f(TOrm{AAc4+;OXG)YlWw*Dt<#*<+Q9k%qGzswxt^1A(>0W@KsQevr=HDa-=W! zhc^IqKqY0B$IiuU$7D6gTBpmZp99_Nf%gpb$yy3t>=coA6}?5({gWS+5^qIUu`lNp zSS=76Yos-pySy8kOLwx3UCFqM{cefmm9=uGDq{22x2nlc9aD;hURrjyL5e^2v#dt+ zDE#WMtP=RuyD)*yiF+r#fJ&e-c|B`nh8kE@ht=m>ATo@93WegfTdoSniUv15`JGU|B z{67KQa;=j>ZX;kC2;@Tpc%Mezbdif`;w>H{h+wX8jb@h+!XGYm8KJbeoDcZO6@2VU zR}n@y|GC;VM7Wkn*YOFT@);NT-1TlCifA_y<0dz|#jX5EEOBmgyF1)TEAf26m$bQy zuifn)9_MY|A%TBLB+0$*b3e%*;Abv*2Pqyj&>(|J>OJ1;ecsPclu_;jKIlU}%s3THFvT>JKH{Ui&J6ET$-h)l?PEUf6V&)5zi`>7 zxSv{|_8Fh`IqH~Yo(1N7-WPn)m#C-VIzjMY9R>ge0Px=oRWw%Ez;v!L3ApN3`KFKScy_)%2lXTrCNQuS!9qyfGfmuh6ty5#un0uC!2ip%(uWoQb?tga`Gs!$YL_>##OFS#B*M7og3Wb z7MHlpLmm-FvfYWXhdtTI0}|}Tc{Xv_-lX%5@7yND9>N4U#6b>lfeOx2Xdm`-lH(lX zoP9Y;A}{S{e+M|wK@N6^LmlRD?l^)dNo-{^JJ`-nHc-YF*7MYnj&ihP9P2nsEVayX zD;)0xCpyW=PI0Q!obC)~I*Z-BqnKTMg)VZjOI+$Q zm%GB1u5z_&TjDdC0?t zho$J3u&w^f_5*6fVdW^#+r0+gNWgF6ax;RVLcmSj85|}SoO(>`u)(L<9F?b{%QT!i z;MO$J^EAs;@Lrc!&zXGxj88-bo&@H3vdi;C@Wg(!T~#I0_W ztEESoO|tJ_uU@2oS5j!%2=5)DR$GtjKt1x$Vjmp9%!>{*=K5GA##rMofNrI zluMDD9C{KtFGa40*m0|PR<;1+0wPxgk-KM+yB8vNzZJQA7P&_ua*r%>Pb_jzrN}99 zF-nM>DmNo29yG2CjvOLY4v`Y?B@tZ`@e~sI6dH3OYFg~jLe%swFD-a!$xBO7TkeTk zj{~Wg;B>ckDr#eL;OvXoh_~<{0q=>}lmkX2nLQC3A|O6LD_GiHmR7uYyZ_=bm&g@G zc~F7(L=5NQxdj$Cm;aB5O*qZ(iC9l&LC6bnq+<6-g*Z~Ne)Du(kz|@1O2UE{XJ(v>UiXKe;qQ>>81yu}yY7(fU zDlwtDjHoi_QSFe2>I6}r5~)K#&4p8mPpvs>w>#W$ZtR*GGN6V8(2yWEWJC=KQNsYx zkRTco>UGq>yi z06ivVn*aa-00IC101tQpEsV8c15pq}->jLPS}bLv{D-GZsh|NdqoIf^;>t19VW`7L z$?V`F;1(DI#y#Q#@x@Pwe_9|dwZpX60qI%KA-?G?L|=Z0fdXU~b{S&93L!dkA-1f~ zi4ASX36GqUp+XIZM;lAOf;c5yCs!WEIoC)vWZl$t>20R>_zw?W5N3D*Y|aCa>_8L+ z(0j944boGYeA~8d+qP}nwr$(CZQHi}-M3RL!s75!qV0yQO;DOXxz#d2jNR+d?b6-!A3{{4dnfY4es>DU%Vn+6?Q z!2&?QKgtZe;BYe-|1Ir z29Dov7nw8HZvcdBDVPgKqg^^^014 zB&%jb__ZqoHJ@s_VV|@YKH-ffUY#aczpa8wB3Y>dfexOEmM){*r{A*f{EGf1iuM;u zxG{JNyg;H}R4uAsJd{j7$keoO9qV2Z*Ab)J?y#B25Q_JE%-_EZnuw$jxcE-mF%ga{ zI+4zsNpzo)M9!0Cn2D8a7!k^oTxvkmFqbg?LkA|<8QYc+Y=5Ujz&-kQPzw37=zS(>ra*XrPFba2BV@g4PRpyb6}ww>u(oF zK;pyN;^P4EQICpcAJfwpA1jLU!hYLAJrpA==xh>HNai;a)M##qG>1r<>SO;LTCgY( zm4z83N=B#to=_$n-vgYVvD9pN?}g*_a{p?E34K5A`Excoc>VT&uZWC%yRO$%mrH$E z60!c<{q%53mIKt7Y$IOqaXV05_kRwzm1!5EOYAAQFt>pB*0JsQqD97RY0eFQ%942a z4}D_Mwd6p|!En9a%kwfV>#LG)E%rMjf20Q-?nsLgdrW1YE_gDV4PGw(j9rvJ_I!iH zznzMIz_zgP6RjcAugemsvZao{hP{@%noI1A%=J7ira-1uL?y^D8`>nxartg)OVd$m z8_&@@+fpLtyMa-xK;k;tarTO}B0#EQt-xfC(M7?Lqp3nX%|EG*RoyrVM9=jst+n1<(0Q7Z;pe=n=v?$VU{5Isjc4c< z9q_FX+gg}`Y?xIM5LOps{Y0l9Vq^w{R&ZMJsu_&%r8P_$fDOheX6oaVlJ>#QOagZz z)RD9&mLKCFR@9gL6T6qpS=#z!HfWzI{U(ylF6>(LzefWyhB>!+(tXG5QLqfhxbL3_ zjk{dGWy<3yjd4G+67ruLl|*`cx$SsFp}}rFT1Q;l2ORxVVbB7&5m}TMog}8mD(5vb z9UUZm66diEnD#-~H?aMV2dis=uC?af)V07<6dFdMU9!pAyP~cU-!+8|Xx}Kb2MirJ zfOcm?`w`Gle<)lA*DZsAYKv@8q+12(mIcV!qS~n_NMf&aD9R);awyKl72U(6Q`F(W zCHVhe0zI77oNiDYEC7$|r>IMO61y7H)Ky@qCDVASnOr6y=rU+{Cp6)Pdxt;VneIF& zS>IW3=B-XiNL>=egH!D(3T>EKonBRO9vVf*vb={VS}M0yQjZh4sO6-8OhSq_D%PXPIXKw0&j~a8Y`*iPDsbApQZpQIw}z!=Hk}Y~pJV<5zo)=PA^WEA+8FQOaUlC_r%hM~w8H5p^cxj_a; zW#)?`S`|1qjW5={nnt; zZVQ<(Yn*B%``T|RdRg5b4UpMSwapiQ!)ZYm%~qJgH_{rtbZE(aI|oM*D^%(uQ!`Pk zG1XIOF}-3(-aS$JT(0A2#~#IRf7BbK6X8sr;djCOJ#Vv7Ha$!ddTJU`&&^&^=+YB++n z%pbs|mLK-X{aRyJ=AGYU_THEUb3;b2$u@8POHLbJrj+-JBHne`5{I_5w<15y$-=OT z(VjnhW^w9)^A1RoWZWXXaP#ks z9kxg;Eu57!1&z(TN-7yjU{cC^t}vog{=OKPOak)yxAao6yA(<}@5T(D`o)S=q6Gnu zBH{VB3YXKZ)Y}+JA_Hzf*Cg$nZR{Ok_)9ML$WfCu_SqH~jO~xToa=U}hd(;F;V6TmPY0Ni)+B@%?mIfadbC=dA%uE3C+1Rz1Kf{B6*w3#T z^*pr5^4Pbgk=7P&d|Uq&E3VYsM>dMt(^R@}X49(qEY3ko9{aQ#^KFej^TuygNt3p} z35r4wWiI_Dtj_~E$JN!UR7M(zc+^T|ZB;Ie12|-Bz_r396xN_zqE&=}}m;QIz>d5{%zCUcOoqV2Fw+ zmiHQfY)#bnP(b*3Xp!bKZEY*>UG@^!!u4o~`|jr(qz1-NL}vilJ%E+2EtAKN{?d%-E~I}eX;#fOjVF509OB`R0MjAN>%8Xglq8>M3h z@&9poG=iRWiP(TPrOKqfI9CM8ng7ik``e`waD9GUN|Stdvj3X$p3pI1Drn^aDgRSO zN(ehNpE9gkvu}Bh^z9KzlRNka#5nZN68m9rvYV*ar~DtJu-=!d=Aj35w4y~ExH8-^ z-HFb`FjuhacjM3o9<1nz?&_X?XJW2iPz&|!s*HHZ!3mYw#IW}X*WdMk&*2Y8gaZvd z)jVGjuIOWHU&X1gmRPD*-NdOmeCcZY?U1%oi-&q^dbrz~i?B7SukFH7#cr<$-NlbK zsI3MQj;*!)?n~SJtA^;5ePlvTF3Fm zcR3~QjB382>l$dkqkBf%Zh1|Fvg4bb*sO9TAWz*Sk7?|X1px*JIwvM zG@L;Z+KpQ=%{j=lU(4rj=H_LO?~*XkRBik|V`rI90VK37yGb+CQNY-Pb>%8C}G;KruA`%wzNBYp$u)L{9#|KX8JobmANR-#1v-u|^ zpv&RUe;N8+yyC;*!QlMD4LuKayi@fs|HUbk`6qN~Kao)ejX6lLVBZ^!5hR_|euj}? zckCQ59%6sr9l|J9UkvlIv+wxRD>;OS3C?AmkF3v)0Ldlx3S;||+O=-YKpW}44>AP$nt#M(g_%8{^$-_=`A~fEuAaBh zy6oMHL1|p-DcjGDkiSjfl3A3(GQSeB1af6XZLS%(x1Ld};3-V5Bn!0t_$Sf(KOrE9 z(!~+KtgK#Q&*LaN+kB`XP{2dpTVBXWm`{|?YIe>*itO`ChHjrPab@%J>-3-L3~*X3g;xacLrcx{cX$SfGfkW7)9==?E|YlF!hdRCb6 zTB*?rm!2dhQ#n0zo~Th!jJ`dj^ILeTy``ZINpipULXws4$ArnAdIqe9B<1XJDr_v| z&XT?@#87T#(FNevGj?iG*4?a|Jdf2}yluymaC~jN{gCpjd8eL3e)14@IxG)we%x=l0*Ws{3#rGPl&)zUy{8 znRK{?y*S0o=>-X%^ZKax`1s)I4vt-|f-C#cUO_lTlpP(ADf=s#R;w%fQBNV9Y|0?pC&5Xs!l=E!!8>Z_Art}R&pK?ixm)gHjPp$I z0h#z>JE@_zDpxLY)1%-jcE{1M`c|g{Np_ZzJ@*+8)sEePM7uC z+e#!6^uaJ#qDD zeWa1s_6#o`PU7e)j^XRalf(+tmq}@#Vp7gsD7}1 z@XEJ$T6m$rI^YcTmNMdL;J+`X{86q*yp3r32hUGaU@-wAoWd6ToN;<>hS@iv0m^C#2aDSNJl?UnMjFRp8! z_%6b49w-<&SpW=o{11qvFjkKt?6s1hlK3?~31?O3&Uc)$=4>J|{7YqxING%_lszoJ zcz)?8>fG36;6L&@g)ftLNZcSd4|)!I5I^^RqaW_)?m)zxl~$@RE@>oG+<8E+TM*qX z60R=io`3leiSu7uUd+ky-Cs^RCgdv_=e8uR=`B=x0E)Mo8OkIvfO45=QgU*RcE$x` zr|p;t6C&8m2eb(63kao!LiwF~rXn$SbarW{f{MO_TJm{4)Wn!l>? zv<21#)`&%BxsFbFt~60P_;Xj*=51=~9-W-esO{nO0K6;KBk9HVmKUzcuJCo{HOYSJ zug1!hlxbKHob zgldiO2soZ=0>N!!gvtGxX`ZZ~1R`)2*>K<1_C5$F>0p2V*wgE{VOC|z03%|n%g!L( zIUr0^T#`#7ajFVkk8sINXEv+qXwgksBg>O!v!%^@P%AFr_Nrp|o6P-f+bN#VfpEFE zMorJ-gA%zk8A|Xs3_=Bd1y)twDDB{+l%mE_He+RH^}^Ot+Tr_6U2Ckdx1^+``LKEw zHwW#ZOnx@HOi1#Ey}I@JW?u=a7f>TMyDyM5V#DV1`XMT8kVPYEr*a1buWfN&lI~-9|ict1&=Jqs-5zIfY@7XV| zT>DQ7Tti&N24Agp)R^Ktid25q=_v0jZhop*etPhVynXOjipoR^aAa0xW)?Wqs4~?E9J_E5x@J zZuEW2s4#Uz^Lu?tHPzPsH{gB>@E``Zp94Dx=GxcjC~dxs>#SPY_nzryUSQ02(sruU zXxJ0%wOinfZbnsWWK?S+QiJfTHO~tUZZC*uyI|qXGH@&%^IJv4R-M)B6ws%Zs|r+V zM=$(dQ^{L5%qynBaZo4Bnu?lfETsOkC#}>rk?MR=@Q--@4H_T?KgJ5%njDV0{*+t! z&KFDGmn==dR^p!OXtIOW+%ismqP%yK&7UnNzKJXPCQS5ravy{@h{&a@u2 z#CMj9lCwrtFhtQ20z81oP(oDFJq`6eb!R&JJ&oZ9FFVT9FWQ!;{bz*Q({>Ymd^)seDaJ;YT!uhc#*rT|>N(r)ds)}tm73^Ne*sS&0qlfc;t>{Z05$Bs5 z3WT|5yPN{dw&WgQLBu*$s+G{n%BDOW2>~sRPntrPlW$@tgf(W=nH=Lt975e2^|B$_ zx1V&%c*EQ$+=!HT`afn^tHd&v*pB=%%bj5AF{;sBC`)du68i>`1w+isJQciOIhM}s zegPg-9^vbS&FmMzcC$L=l{1aX!4-Fe8Ah(>d>bP^rxZBqzF|OaZg9;0KEe7pW=^vUm-v4nk`Q8yX3W2+Y zBE@QcqYPqGdyf648|0++8k@l%q@w;bHqBexK6b!$)y4rmQmxHi`vmB4PMaM^wuzVt zkv4eE&@e>N)pRS*mXP&Zuon`b@+-BXLgx2$8V6=F>|ec5b#t_*~21te8=lHCB&A^ih<9W}^j(-!iZ}#iMVDgZb zv}a%yscmBM7qV{`eBH_lvutsA$A3p?twP6y&d$g0?(WThE1(Xg8bb#VIh1N zaweC64w_5{xH-LU>^f>{6;W#(2E9SQ zl76G2QnE9SgSD%xkNL=hFSz>&xCaq&`x&E%qUwe3e-;XJ7fP2Fl56vCf8OSnn@wcu zCj`ofl5IN{jROl015#PEy9!ngJ^IgFM1$avUW}VAmXTk)R+mt-b(Plh4Ey_Kuo6!_ zA0>VP>(+hc>yOL>wY)t1XDh`U9-9w=dh%=$4yf?u2*FA_LUcYDz8O1RJJmk@4qm^Ou9}Xo*s+CZC#O K{!uM@TK@-qbnXNI literal 0 HcmV?d00001 diff --git a/src/resources/fonts/Syne/Syne-Bold.woff2 b/src/resources/fonts/Syne/Syne-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b9d6b2a7e7e1a376f214733ccefcdf5c4f8f341e GIT binary patch literal 31564 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 bQ8&KJY4PA6iu@gE$2B};|I|=(?fk_= literal 0 HcmV?d00001 diff --git a/src/resources/fonts/Syne/Syne-ExtraBold.woff b/src/resources/fonts/Syne/Syne-ExtraBold.woff new file mode 100644 index 0000000000000000000000000000000000000000..5c6c7ea73004fd07d8384b1155b522c18eb55f03 GIT binary patch literal 40220 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 J00961004^+Ufloy literal 0 HcmV?d00001 diff --git a/src/resources/fonts/Syne/Syne-Italic.woff2 b/src/resources/fonts/Syne/Syne-Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..8fd2c3d0f412eb7f3d28441847be43825001f6e3 GIT binary patch literal 60680 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

  • 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 literal 0 HcmV?d00001 diff --git a/src/resources/fonts/Syne/Syne-Regular.woff b/src/resources/fonts/Syne/Syne-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..4715323031833f62a82a28a19a024ee3742a60dd GIT binary patch literal 36604 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�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 I0RR9102InkrT_o{ literal 0 HcmV?d00001 diff --git a/src/resources/fonts/Syne/Syne-Regular.woff2 b/src/resources/fonts/Syne/Syne-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a0fd4b27085f1f3086fc4df028753a9c645efdd9 GIT binary patch literal 29164 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

    VxMjcJqfDa!i-0V)omNakfxTcxsyQ_?-ot}3&# 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

    diff --git a/src/resources/views/components/header.blade.php b/src/resources/views/components/header.blade.php new file mode 100644 index 0000000..ea68349 --- /dev/null +++ b/src/resources/views/components/header.blade.php @@ -0,0 +1,125 @@ +@php + $user = auth()->user(); + + // Aktuelle Seite aus Sidebar-Config ermitteln + $allItems = array_merge( + config('sidebar.main', []), + config('sidebar.tools', []), + config('sidebar.admin', []), + ); + $currentLabel = null; + foreach ($allItems as $item) { + if (!empty($item['route']) && request()->routeIs($item['route'])) { + $currentLabel = $item['label']; + break; + } + } + // Fallback fΓΌr Unterseiten + if (!$currentLabel) { + $currentLabel = match(true) { + request()->routeIs('agent.logs') => t('header.agent_history'), + request()->routeIs('subscription.index') => t('header.subscription'), + request()->routeIs('admin.users.detail') => t('header.user_details'), + request()->routeIs('admin.users.calendar') => t('header.user_calendar'), + default => null, + }; + } + +@endphp + +
    + + {{-- LINKS: Toggle + Seitenname --}} +
    + + {{-- Mobile: ΓΆffnet Overlay | Desktop: collapsed/expanded --}} + + + + + @if($currentLabel) + {{ t($currentLabel) }} + @endif + +
    + + + {{-- RECHTS: Notifications + User --}} +
    + +
    diff --git a/src/resources/views/components/logo.blade.php b/src/resources/views/components/logo.blade.php new file mode 100644 index 0000000..d482842 --- /dev/null +++ b/src/resources/views/components/logo.blade.php @@ -0,0 +1,67 @@ +@props([ + 'mode' => 'dark', +]) + + + + + + + + + + +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{----}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{----}} diff --git a/src/resources/views/components/sidebar.blade.php b/src/resources/views/components/sidebar.blade.php new file mode 100644 index 0000000..e527608 --- /dev/null +++ b/src/resources/views/components/sidebar.blade.php @@ -0,0 +1,239 @@ +@php + $user = auth()->user(); + $subscription = $user->subscription()?->latest()->first(); + $plan = $subscription?->plan; + $creditLimit = $user->effective_limit; + $creditsUsed = $user->effective_usage; + $usagePercent = $user->usage_percent; + $initial = strtoupper(substr($user->name ?? 'U', 0, 1)); +@endphp + +
    + + {{-- LOGO --}} +
    + +
    + + + {{-- NAVIGATION --}} +
    + + {{-- HAUPT-NAV --}} + + + + {{-- TOOLS --}} +
    +

    + {{ t('nav.config') }} +

    + +
    + + + {{-- ADMIN (Support+) --}} + @if($user->hasAtLeastRole('support')) +
    +

    + + Admin +

    + +
    + @endif + +
    + + + {{-- FOOTER --}} +
    + + {{-- Plan + Credits --}} + @if($plan) +
    + @php + $sidebarCreditBadge = match(true) { + $usagePercent >= 100 => [ + 'label' => t('nav.full'), + 'bg' => '#FEF2F2', + 'color' => '#991B1B', + 'dot' => '#EF4444', + ], + $usagePercent >= 80 => [ + 'label' => number_format($creditsUsed) . ' / ' . number_format($creditLimit), + 'bg' => '#FFFBEB', + 'color' => '#92400E', + 'dot' => '#F59E0B', + ], + $usagePercent >= 60 => [ + 'label' => number_format($creditsUsed) . ' / ' . number_format($creditLimit), + 'bg' => '#FFF7ED', + 'color' => '#9A3412', + 'dot' => '#FB923C', + ], + default => [ + 'label' => number_format($creditsUsed) . ' / ' . number_format($creditLimit), + 'bg' => '#EEF2FF', + 'color' => '#3730A3', + 'dot' => '#4F46E5', + ], + }; + @endphp +
    + {{ $plan->name }} + @if($creditLimit > 0) + + + {{ $sidebarCreditBadge['label'] }} + + @else + + + {{ t('nav.unlimited') }} + + @endif +
    + + @if($creditLimit > 0) +
    +
    +
    + @endif + + @if(!($plan->is_internal ?? false)) + + {{ t('nav.manage_plan') }} + + @endif +
    + @endif + + + {{-- User + Logout --}} +
    + + {{-- Avatar --}} +
    + {{ $initial }} +
    + + {{-- Name + Email --}} +
    +

    {{ $user->name }}

    +

    {{ $user->email }}

    +
    + + {{-- Settings + Logout --}} +
    + + + +
    + @csrf + +
    +
    + +
    + +
    + +
    diff --git a/src/resources/views/components/sidebar/link.blade.php b/src/resources/views/components/sidebar/link.blade.php new file mode 100644 index 0000000..dc33a1c --- /dev/null +++ b/src/resources/views/components/sidebar/link.blade.php @@ -0,0 +1,30 @@ +@props([ + 'href', + 'method' => 'GET', + 'active' => false, +]) + +@if($method === 'POST') +
    + @csrf + +
    +@else + merge(['class' => 'flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ' . + ($active + ? 'bg-indigo-50 text-indigo-700 font-medium' + : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700') + ]) }} + > + {{ $slot }} + +@endif diff --git a/src/resources/views/emails/affiliate-qualified.blade.php b/src/resources/views/emails/affiliate-qualified.blade.php new file mode 100644 index 0000000..7e093b2 --- /dev/null +++ b/src/resources/views/emails/affiliate-qualified.blade.php @@ -0,0 +1,28 @@ +@extends('emails.layout') + +@section('content') + +
    + +
    + +

    + Du hast {{ $credits }} Credits verdient! +

    + +

    + {{ $referredUser->name }} nutzt Aziros jetzt seit 3 Monaten + β€” dein Referral hat sich qualifiziert! +

    + +
    +
    +{{ $credits }}
    +
    Credits wurden deinem Konto gutgeschrieben
    +
    + + + Mein Affiliate-Dashboard + + +@endsection diff --git a/src/resources/views/emails/agent/message.blade.php b/src/resources/views/emails/agent/message.blade.php new file mode 100644 index 0000000..b2f4079 --- /dev/null +++ b/src/resources/views/emails/agent/message.blade.php @@ -0,0 +1,26 @@ +@extends('emails.layout') + +@section('content') + +
    + +
    + +

    + Nachricht +

    + +

    + von {{ $sender_name }} +

    + +

    + Nachricht an {{ $recipient_name }} +

    + +
    +

    {{ $message }}

    +
    + +@endsection diff --git a/src/resources/views/emails/agent/reminder.blade.php b/src/resources/views/emails/agent/reminder.blade.php new file mode 100644 index 0000000..3b482f0 --- /dev/null +++ b/src/resources/views/emails/agent/reminder.blade.php @@ -0,0 +1,56 @@ +@extends('emails.layout') + +@section('content') + +
    + +
    + +

    + Terminerinnerung +

    + +

    + von {{ $sender_name }} +

    + + {{-- Event Details --}} +
    + + + + + + + + + + + + + + @if(!empty($event_notes)) + + + + + @endif +
    Termin{{ $event_title }}
    Datum{{ $event_date }}
    Uhrzeit + {{ $event_time }}@if(!empty($event_end)) - {{ $event_end }}@endif Uhr +
    Info{{ $event_notes }}
    +
    + + {{-- PersΓΆnliche Nachricht --}} + @if(!empty($message)) +
    +

    Nachricht

    +

    {{ $message }}

    +
    + @endif + +

    + Hallo {{ $recipient_name }}, dies ist eine automatische Terminerinnerung. +

    + +@endsection diff --git a/src/resources/views/emails/aria-composed.blade.php b/src/resources/views/emails/aria-composed.blade.php new file mode 100644 index 0000000..f6cbf2c --- /dev/null +++ b/src/resources/views/emails/aria-composed.blade.php @@ -0,0 +1,3 @@ +

    {!! nl2br(e($body)) !!}

    +
    +

    Gesendet von Aria Β· aziros.com

    diff --git a/src/resources/views/emails/auth/verify.blade.php b/src/resources/views/emails/auth/verify.blade.php new file mode 100644 index 0000000..53f730c --- /dev/null +++ b/src/resources/views/emails/auth/verify.blade.php @@ -0,0 +1,29 @@ +@extends('emails.layout') + +@section('content') + +
    + +
    + +

    + {{ t('mail.auth.verify.title') }} +

    + +

    + {{ t('mail.auth.verify.text') }} +

    + + {{-- BUTTON --}} + @if(!empty($url)) + + {{ t('mail.auth.verify.button') }} + + @endif + +

    + {{ t('mail.auth.verify.expires') }} +

    + +@endsection diff --git a/src/resources/views/emails/components/button.blade.php b/src/resources/views/emails/components/button.blade.php new file mode 100644 index 0000000..37ea4af --- /dev/null +++ b/src/resources/views/emails/components/button.blade.php @@ -0,0 +1,14 @@ + diff --git a/src/resources/views/emails/components/code.blade.php b/src/resources/views/emails/components/code.blade.php new file mode 100644 index 0000000..ead7e97 --- /dev/null +++ b/src/resources/views/emails/components/code.blade.php @@ -0,0 +1,25 @@ + + + @foreach(str_split($slot) as $index => $digit) + + + + {{-- Abstand --}} + @if(!$loop->last) + + @endif + + @endforeach + +
    + {{ $digit }} +
    diff --git a/src/resources/views/emails/components/footer.blade.php b/src/resources/views/emails/components/footer.blade.php new file mode 100644 index 0000000..c8083ae --- /dev/null +++ b/src/resources/views/emails/components/footer.blade.php @@ -0,0 +1,12 @@ + + + + Du erhΓ€ltst diese E-Mail, weil du {{ config('app.name') }} nutzt.
    + Falls das nicht du warst, kannst du diese Nachricht ignorieren. + +
    + Β© {{ date('Y') }} {{ config('app.name') }} +
    + + + diff --git a/src/resources/views/emails/components/header.blade.php b/src/resources/views/emails/components/header.blade.php new file mode 100644 index 0000000..481d4d7 --- /dev/null +++ b/src/resources/views/emails/components/header.blade.php @@ -0,0 +1,11 @@ + + + + + +{{--
    --}} +{{-- {{ config('app.name') }}--}} +{{--
    --}} + + + diff --git a/src/resources/views/emails/gift-access.blade.php b/src/resources/views/emails/gift-access.blade.php new file mode 100644 index 0000000..35930be --- /dev/null +++ b/src/resources/views/emails/gift-access.blade.php @@ -0,0 +1,35 @@ +@extends('emails.layout') + +@section('content') + +
    + +
    + +

    + Hallo {{ $user->name }}! +

    + +

    + Du hast kostenlosen Zugang zum + {{ $plan->name }} + erhalten β€” {{ $durationLabel }}. +

    + + @if($endsAt) +

    + Dein Zugang ist gΓΌltig bis: + {{ $endsAt->format('d.m.Y') }} +

    + @else +

    + Dein Zugang ist unbegrenzt gΓΌltig. +

    + @endif + + + Jetzt Aria nutzen + + +@endsection diff --git a/src/resources/views/emails/layout.blade.php b/src/resources/views/emails/layout.blade.php new file mode 100644 index 0000000..f072db2 --- /dev/null +++ b/src/resources/views/emails/layout.blade.php @@ -0,0 +1,31 @@ + + + + + + + + +
    + + + + {{-- HEADER --}} + @include('emails.components.header') + + {{-- CONTENT --}} + + + + + {{-- FOOTER --}} + @include('emails.components.footer') + +
    + @yield('content') +
    + +
    + + + diff --git a/src/resources/views/emails/reset-password.blade.php b/src/resources/views/emails/reset-password.blade.php new file mode 100644 index 0000000..201341c --- /dev/null +++ b/src/resources/views/emails/reset-password.blade.php @@ -0,0 +1,63 @@ + + + + + + Passwort zurΓΌcksetzen + + + + + + +
    + + + {{-- Logo --}} + + + + + {{-- Card --}} + + + + + {{-- Footer --}} + + + + +
    + aziros +
    + +

    + Passwort zurΓΌcksetzen +

    +

    + Hallo {{ $user->name }}, +

    +

    + Du hast ein ZurΓΌcksetzen deines Passworts angefordert. + Klicke auf den Button, um ein neues Passwort zu setzen. +

    + + + Passwort zurΓΌcksetzen + + +

    + Dieser Link ist 60 Minuten gΓΌltig.
    + Falls du kein Passwort-Reset angefordert hast, ignoriere diese E-Mail. +

    + +
    + Β© {{ date('Y') }} Aziros Β· Made in Austria +
    +
    + + diff --git a/src/resources/views/emails/smtp-test.blade.php b/src/resources/views/emails/smtp-test.blade.php new file mode 100644 index 0000000..d3a5c1f --- /dev/null +++ b/src/resources/views/emails/smtp-test.blade.php @@ -0,0 +1,2 @@ +

    Dein SMTP Server ist korrekt konfiguriert.

    +

    E-Mails kΓΆnnen von Aria in deinem Namen gesendet werden.

    diff --git a/src/resources/views/errors/400.blade.php b/src/resources/views/errors/400.blade.php new file mode 100644 index 0000000..4209a4b --- /dev/null +++ b/src/resources/views/errors/400.blade.php @@ -0,0 +1,44 @@ + +
    + + {{-- Background glows --}} +
    +
    +
    + +
    + + {{-- Logo --}} +
    +
    + +
    + aziros +
    + + {{-- Error Code --}} +

    + 400 +

    + + {{-- Text --}} +

    {{ t('errors.400.title') }}

    +

    {{ t('errors.400.message') }}

    + + {{-- CTA --}} +
    + + {{ t('errors.back_home') }} + + + +
    + +
    +
    +
    diff --git a/src/resources/views/errors/401.blade.php b/src/resources/views/errors/401.blade.php new file mode 100644 index 0000000..44bdcd2 --- /dev/null +++ b/src/resources/views/errors/401.blade.php @@ -0,0 +1,44 @@ + +
    + + {{-- Background glows --}} +
    +
    +
    + +
    + + {{-- Logo --}} +
    +
    + +
    + aziros +
    + + {{-- Error Code --}} +

    + 401 +

    + + {{-- Text --}} +

    {{ t('errors.401.title') }}

    +

    {{ t('errors.401.message') }}

    + + {{-- CTA --}} +
    + + {{ t('errors.back_home') }} + + + +
    + +
    +
    +
    diff --git a/src/resources/views/errors/403.blade.php b/src/resources/views/errors/403.blade.php new file mode 100644 index 0000000..304c65b --- /dev/null +++ b/src/resources/views/errors/403.blade.php @@ -0,0 +1,44 @@ + +
    + + {{-- Background glows --}} +
    +
    +
    + +
    + + {{-- Logo --}} +
    +
    + +
    + aziros +
    + + {{-- Error Code --}} +

    + 403 +

    + + {{-- Text --}} +

    {{ t('errors.403.title') }}

    +

    {{ t('errors.403.message') }}

    + + {{-- CTA --}} +
    + + {{ t('errors.back_home') }} + + + +
    + +
    +
    +
    diff --git a/src/resources/views/errors/404.blade.php b/src/resources/views/errors/404.blade.php new file mode 100644 index 0000000..5f457ed --- /dev/null +++ b/src/resources/views/errors/404.blade.php @@ -0,0 +1,44 @@ + +
    + + {{-- Background glows --}} +
    +
    +
    + +
    + + {{-- Logo --}} +
    +
    + +
    + aziros +
    + + {{-- Error Code --}} +

    + 404 +

    + + {{-- Text --}} +

    {{ t('errors.404.title') }}

    +

    {{ t('errors.404.message') }}

    + + {{-- CTA --}} +
    + + {{ t('errors.back_home') }} + + + +
    + +
    +
    +
    diff --git a/src/resources/views/errors/500.blade.php b/src/resources/views/errors/500.blade.php new file mode 100644 index 0000000..5a0bb0a --- /dev/null +++ b/src/resources/views/errors/500.blade.php @@ -0,0 +1,44 @@ + +
    + + {{-- Background glows --}} +
    +
    +
    + +
    + + {{-- Logo --}} +
    +
    + +
    + aziros +
    + + {{-- Error Code --}} +

    + 500 +

    + + {{-- Text --}} +

    {{ t('errors.500.title') }}

    +

    {{ t('errors.500.message') }}

    + + {{-- CTA --}} +
    + + {{ t('errors.back_home') }} + + + +
    + +
    +
    +
    diff --git a/src/resources/views/errors/501.blade.php b/src/resources/views/errors/501.blade.php new file mode 100644 index 0000000..34b9c03 --- /dev/null +++ b/src/resources/views/errors/501.blade.php @@ -0,0 +1,44 @@ + +
    + + {{-- Background glows --}} +
    +
    +
    + +
    + + {{-- Logo --}} +
    +
    + +
    + aziros +
    + + {{-- Error Code --}} +

    + 501 +

    + + {{-- Text --}} +

    {{ t('errors.501.title') }}

    +

    {{ t('errors.501.message') }}

    + + {{-- CTA --}} +
    + + {{ t('errors.back_home') }} + + + +
    + +
    +
    +
    diff --git a/src/resources/views/errors/502.blade.php b/src/resources/views/errors/502.blade.php new file mode 100644 index 0000000..7ba26f3 --- /dev/null +++ b/src/resources/views/errors/502.blade.php @@ -0,0 +1,44 @@ + +
    + + {{-- Background glows --}} +
    +
    +
    + +
    + + {{-- Logo --}} +
    +
    + +
    + aziros +
    + + {{-- Error Code --}} +

    + 502 +

    + + {{-- Text --}} +

    {{ t('errors.502.title') }}

    +

    {{ t('errors.502.message') }}

    + + {{-- CTA --}} +
    + + {{ t('errors.back_home') }} + + + +
    + +
    +
    +
    diff --git a/src/resources/views/layouts/app.blade.php b/src/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..a0fc30c --- /dev/null +++ b/src/resources/views/layouts/app.blade.php @@ -0,0 +1,102 @@ + + + + + + + @yield('title', config('app.name')) + @vite(['resources/css/app.css']) + @livewireStyles + + + + +
    + + {{-- DESKTOP SIDEBAR --}} + + + {{-- MOBILE SIDEBAR OVERLAY --}} +
    + +
    + +
    + +
    +
    + + {{-- MAIN --}} +
    + +
    + @hasSection('content') + @yield('content') + @else + {{ $slot ?? '' }} + @endif +
    +
    + +
    + +@php $currentVersion = \App\Models\AppVersion::current('web'); @endphp +@if($currentVersion?->status === 'beta') +BETA +@endif + + + +@livewireScripts +@livewire('wire-elements-modal') +@vite(['resources/js/app.js']) + + diff --git a/src/resources/views/layouts/blank.blade.php b/src/resources/views/layouts/blank.blade.php new file mode 100644 index 0000000..1558407 --- /dev/null +++ b/src/resources/views/layouts/blank.blade.php @@ -0,0 +1,42 @@ + + + + + + + {{ config('app.name') }} + + + + + + + @vite(['resources/css/app.css']) + @livewireStyles + + + + +{{-- BACKGROUND --}} +
    + + {{-- Gradient --}} +
    + + {{-- Glow --}} +
    +
    + +
    + +{{-- CONTENT --}} +
    + {{ $slot }} + +
    + +@livewireScripts +@vite(['resources/js/app.js']) + + + diff --git a/src/resources/views/livewire/activities/index.blade.php b/src/resources/views/livewire/activities/index.blade.php new file mode 100644 index 0000000..b595871 --- /dev/null +++ b/src/resources/views/livewire/activities/index.blade.php @@ -0,0 +1,241 @@ +@php + $filterOptions = [ + '' => ['label' => t('activities.filter.all'), 'icon' => 'squares-2x2'], + 'event' => ['label' => t('activities.filter.events'), 'icon' => 'calendar'], + 'reminder' => ['label' => t('activities.filter.reminders'), 'icon' => 'bell'], + 'automation' => ['label' => t('activities.filter.automations'), 'icon' => 'bolt'], + 'contact' => ['label' => t('activities.filter.contacts'), 'icon' => 'user'], + 'note' => ['label' => t('activities.filter.notes'), 'icon' => 'document-text'], + 'task' => ['label' => t('activities.filter.tasks'), 'icon' => 'check-circle'], + 'integration' => ['label' => t('activities.filter.integrations'), 'icon' => 'link'], + 'system' => ['label' => t('activities.filter.system'), 'icon' => 'cog-6-tooth'], + ]; + + $today = now($tz)->toDateString(); + $yesterday = now($tz)->subDay()->toDateString(); +@endphp + +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('activities.title') }}

    +

    {{ t('activities.subtitle') }}

    +
    + @if($activities->total() > 0) + + @endif +
    + + + {{-- STATS --}} + @if($stats && $stats->total > 0) +
    + @foreach([ + ['label' => t('activities.total'), 'value' => $stats->total, 'icon' => 'squares-2x2', 'color' => 'text-gray-600', 'bg' => 'bg-gray-100'], + ['label' => t('activities.today'), 'value' => $stats->today, 'icon' => 'sun', 'color' => 'text-amber-600', 'bg' => 'bg-amber-50'], + ['label' => t('activities.events'), 'value' => $stats->events, 'icon' => 'calendar', 'color' => 'text-indigo-600', 'bg' => 'bg-indigo-50'], + ['label' => t('activities.automations'),'value' => $stats->automations, 'icon' => 'bolt', 'color' => 'text-purple-600', 'bg' => 'bg-purple-50'], + ] as $stat) +
    +
    + @if($stat['icon'] === 'squares-2x2') + @elseif($stat['icon'] === 'sun') + @elseif($stat['icon'] === 'calendar') + @else + @endif +
    +
    +

    {{ number_format($stat['value']) }}

    +

    {{ $stat['label'] }}

    +
    +
    + @endforeach +
    + @endif + + + {{-- FILTER + SUCHE --}} +
    + + {{-- Suche --}} +
    + + +
    + + {{-- Typ-Filter --}} +
    + @foreach($filterOptions as $key => $opt) + + @endforeach +
    + +
    + + + {{-- TIMELINE --}} + @if($grouped->isEmpty()) + + {{-- EMPTY STATE --}} +
    +
    + +
    +

    {{ t('activities.no_activities') }}

    + @if($search || $filterType) +

    {{ t('activities.adjust_filter') }}

    + + @else +

    {{ t('activities.auto_logged') }}

    + @endif +
    + + @else + +
    + @foreach($grouped as $date => $dayActivities) + @php + $label = match($date) { + $today => t('common.today'), + $yesterday => t('activities.yesterday'), + default => \Carbon\Carbon::parse($date)->translatedFormat('l, d. F Y'), + }; + @endphp + + {{-- Datums-Gruppe --}} +
    + + {{-- Datum-Label --}} +
    + {{ $label }} +
    + {{ $dayActivities->count() }} +
    + + {{-- AktivitΓ€ten des Tages --}} +
    + @foreach($dayActivities as $activity) + @php $v = $activity->visual(); @endphp + +
    + + {{-- Icon --}} +
    + @if($v['icon'] === 'calendar') + @elseif($v['icon'] === 'bell') + @elseif($v['icon'] === 'bolt') + @elseif($v['icon'] === 'user') + @elseif($v['icon'] === 'link') + @elseif($v['icon'] === 'document-text') + @elseif($v['icon'] === 'check-circle') + @elseif($v['icon'] === 'arrow-right-on-rectangle') + @else + @endif +
    + + {{-- Inhalt --}} +
    +
    +
    +

    {{ $activity->title }}

    + @if($activity->description) +

    {{ $activity->description }}

    + @endif +
    +
    + + {{ $activity->created_at->setTimezone($tz)->format('H:i') }} + + +
    +
    + + {{-- Typ-Badge + Meta --}} +
    + + {{ $activity->typeLabel() }} + + @if(!empty($activity->meta)) + @foreach(array_slice($activity->meta, 0, 2) as $key => $value) + {{ $key }}: {{ is_array($value) ? implode(', ', $value) : $value }} + @endforeach + @endif +
    +
    + +
    + @endforeach +
    + +
    + @endforeach +
    + + {{-- PAGINATION --}} + @if($activities->hasPages()) +
    + + {{ $activities->firstItem() }}–{{ $activities->lastItem() }} {{ t('activities.of_total', ['total' => $activities->total()]) }} + +
    + @if($activities->onFirstPage()) + β€Ή + @else + + @endif + + @foreach($activities->getUrlRange(max(1, $activities->currentPage() - 2), min($activities->lastPage(), $activities->currentPage() + 2)) as $page => $url) + + @endforeach + + @if($activities->hasMorePages()) + + @else + β€Ί + @endif +
    +
    + @endif + + @endif + +
    diff --git a/src/resources/views/livewire/admin/affiliates/index.blade.php b/src/resources/views/livewire/admin/affiliates/index.blade.php new file mode 100644 index 0000000..bb00dab --- /dev/null +++ b/src/resources/views/livewire/admin/affiliates/index.blade.php @@ -0,0 +1,170 @@ +
    + + {{-- Header --}} +
    +

    {{ t('admin.affiliates.title') }}

    +

    {{ t('admin.affiliates.subtitle') }}

    +
    + + {{-- Stats --}} +
    + @php + $statCards = [ + ['label' => t('admin.affiliates.affiliates'), 'value' => $stats['total_affiliates'], 'icon' => 'users', 'color' => 'indigo'], + ['label' => t('admin.affiliates.active'), 'value' => $stats['active_affiliates'], 'icon' => 'check-circle', 'color' => 'green'], + ['label' => t('admin.affiliates.referrals'), 'value' => $stats['total_referrals'], 'icon' => 'user-plus', 'color' => 'blue'], + ['label' => t('admin.affiliates.pending'), 'value' => $stats['pending_referrals'], 'icon' => 'clock', 'color' => 'amber'], + ['label' => t('admin.affiliates.credits_earned'), 'value' => number_format($stats['credits_paid_out']), 'icon' => 'bolt', 'color' => 'purple'], + ]; + @endphp + + @foreach($statCards as $card) +
    +
    +
    + +
    +
    +

    {{ $card['value'] }}

    +

    {{ $card['label'] }}

    +
    + @endforeach +
    + + {{-- Suche --}} +
    +
    + + +
    +
    + + {{-- Affiliates Tabelle --}} +
    +
    + + + + + + + + + + + + + + + @forelse($affiliates as $aff) + + + + + + + + + + + @empty + + + + @endforelse + +
    {{ t('admin.affiliates.affiliate') }}{{ t('admin.affiliates.code') }}{{ t('common.status') }}{{ t('admin.affiliates.referrals') }}{{ t('admin.affiliates.pending') }}{{ t('admin.affiliates.paid') }}{{ t('admin.affiliates.credits_earned') }}{{ t('common.actions') }}
    +
    +
    + {{ strtoupper(substr($aff->user?->name ?? '?', 0, 1)) }} +
    +
    + + {{ $aff->user?->name }} + +

    {{ $aff->user?->email }}

    +
    +
    +
    + {{ $aff->code }} + + @php + $statusBadge = match($aff->status) { + 'active' => 'bg-green-50 text-green-700', + 'paused' => 'bg-amber-50 text-amber-700', + 'banned' => 'bg-red-50 text-red-700', + default => 'bg-gray-100 text-gray-600', + }; + $statusLabel = match($aff->status) { + 'active' => t('admin.affiliates.active'), + 'paused' => t('admin.affiliates.paused'), + 'banned' => t('admin.affiliates.blocked'), + default => $aff->status, + }; + @endphp + {{ $statusLabel }} + {{ $aff->referrals_count }}{{ $aff->pending_count }}{{ $aff->paid_count }}{{ number_format($aff->total_credits_earned) }} +
    + @if($aff->status !== 'banned') + + + @else + + @endif +
    +
    {{ t('admin.affiliates.none') }}
    +
    +
    + + {{-- Pagination --}} + @if($affiliates->hasPages()) +
    + + {{ $affiliates->firstItem() }}–{{ $affiliates->lastItem() }} {{ t('admin.affiliates.of_total', ['total' => $affiliates->total()]) }} + +
    + @if($affiliates->onFirstPage()) + β€Ή + @else + + @endif + + @foreach($affiliates->getUrlRange(max(1, $affiliates->currentPage() - 2), min($affiliates->lastPage(), $affiliates->currentPage() + 2)) as $page => $url) + + @endforeach + + @if($affiliates->hasMorePages()) + + @else + β€Ί + @endif +
    +
    + @endif + +
    diff --git a/src/resources/views/livewire/admin/dashboard.blade.php b/src/resources/views/livewire/admin/dashboard.blade.php new file mode 100644 index 0000000..bac0697 --- /dev/null +++ b/src/resources/views/livewire/admin/dashboard.blade.php @@ -0,0 +1,247 @@ +@php + $statusClasses = [ + 'success' => ['badge' => 'bg-green-50 text-green-700', 'dot' => 'bg-green-500'], + 'failed' => ['badge' => 'bg-red-50 text-red-700', 'dot' => 'bg-red-500'], + 'error' => ['badge' => 'bg-red-50 text-red-700', 'dot' => 'bg-red-500'], + 'conflict' => ['badge' => 'bg-amber-50 text-amber-700', 'dot' => 'bg-amber-400'], + 'duplicate' => ['badge' => 'bg-blue-50 text-blue-700', 'dot' => 'bg-blue-400'], + 'processing' => ['badge' => 'bg-purple-50 text-purple-700', 'dot' => 'bg-purple-400'], + ]; + + $typeBadges = [ + 'event' => ['#', t('agent.type.event'), '#4F46E5', '#EEF2FF'], + 'event_update' => ['✎', t('agent.type.event'), '#4F46E5', '#EEF2FF'], + 'note' => ['#', t('agent.type.note'), '#B45309', '#FFFBEB'], + 'note_update' => ['✎', t('agent.type.note'), '#B45309', '#FFFBEB'], + 'task' => ['#', t('agent.type.task'), '#065F46', '#ECFDF5'], + 'task_update' => ['✎', t('agent.type.task'), '#065F46', '#ECFDF5'], + 'contact' => ['#', t('agent.type.contact'), '#1E40AF', '#EFF6FF'], + 'email' => ['#', t('agent.type.email'), '#6B21A8', '#FAF5FF'], + 'chat' => ['#', 'Chat', '#374151', '#F3F4F6'], + 'multi' => ['#', t('agent.type.multi'), '#9D174D', '#FDF2F8'], + 'bonus' => ['#', t('agent.type.bonus'), '#065F46', '#ECFDF5'], + 'system' => ['#', t('agent.type.system'), '#374151', '#F9FAFB'], + ]; + + $statusLabels = [ + 'success' => t('agent.status.success'), + 'failed' => t('agent.status.failed'), + 'error' => t('agent.status.error'), + 'conflict' => t('agent.status.conflict'), + 'duplicate' => t('agent.status.duplicate'), + 'processing' => t('agent.status.processing'), + ]; +@endphp + +
    + + {{-- Header --}} +
    +

    {{ t('admin.title') }}

    +

    {{ t('admin.subtitle') }}

    +
    + + {{-- Metric Cards --}} +
    +
    +
    +
    + +
    +
    +

    {{ number_format($totalUsers) }}

    +

    {{ t('admin.total_users') }}

    +
    +
    +
    + +
    +
    +
    + +
    +
    +

    {{ number_format($activeUsers) }}

    +

    {{ t('admin.active_users') }}

    +
    +
    +
    + +
    +
    +
    + +
    +
    +

    {{ number_format($newThisMonth) }}

    +

    {{ t('admin.new_this_month') }}

    +
    +
    +
    + +
    +
    +
    + +
    +
    +

    {{ number_format($proUsers) }}

    +

    {{ t('admin.pro_users') }}

    +
    +
    +
    +
    + + {{-- Cost Cards --}} +
    +
    +
    +
    + +
    +
    +

    {{ number_format($totalCreditsUsed) }}

    +

    {{ t('admin.credits_month') }}

    +
    +
    +
    + +
    +
    +
    + +
    +
    +

    ${{ number_format($totalCost, 2) }}

    +

    {{ t('admin.costs_month') }}

    +
    +
    +
    +
    + + {{-- Recent Logs --}} +
    +
    +

    {{ t('admin.last_activities') }}

    + {{ t('admin.last_10') }} +
    + + @if($recentLogs->isEmpty()) +
    +
    + +
    +

    {{ t('admin.no_logs') }}

    +
    + @else +
    + @foreach($recentLogs as $log) + @php + $sc = $statusClasses[$log->status] ?? ['badge' => 'bg-gray-100 text-gray-500', 'dot' => 'bg-gray-400']; + $badge = $typeBadges[$log->type] ?? ['#', ucfirst($log->type ?? '?'), '#374151', '#F9FAFB']; + + $dur = match(true) { + $log->duration_ms === null => 'β€”', + $log->duration_ms >= 1000 => round($log->duration_ms / 1000, 1) . 's', + default => $log->duration_ms . 'ms', + }; + @endphp + +
    +
    + + {{-- Status Dot --}} +
    + +
    + + {{-- Content --}} +
    + + {{-- Input + Datum --}} +
    +

    {{ $log->input }}

    + + {{ $log->created_at?->format('d.m. H:i') }} + +
    + + {{-- Badges + Metriken --}} +
    + + {{-- User --}} + @if($log->user) + +
    + {{ strtoupper(substr($log->user->name, 0, 1)) }} +
    + {{ $log->user->name }} +
    + | + @endif + + {{-- Typ --}} + + + {{ $badge[1] }}{{ $badge[0] === '✎' ? ' ✎' : '' }} + + + {{-- Status --}} + + {{ $statusLabels[$log->status] ?? $log->status }} + + + | + + {{-- Credits --}} + + + {{ $log->credits }} Credits + + + {{-- Tokens --}} + @if($log->total_tokens > 0) + + + {{ number_format($log->total_tokens) }} + ({{ number_format($log->prompt_tokens) }}↑ {{ number_format($log->completion_tokens) }}↓) + + @endif + + {{-- Dauer --}} + + + {{ $dur }} + + + {{-- Kosten --}} + @if($log->cost_usd > 0) + ${{ number_format($log->cost_usd, 4) }} + @endif + + {{-- Model --}} + @if($log->model) + + {{ $log->model }} + + @endif +
    + + {{-- Fehler-Ausgabe --}} + @if(in_array($log->status, ['failed', 'error']) && !empty($log->output['error'])) +

    + + {{ $log->output['error'] }} +

    + @endif +
    + +
    +
    + @endforeach +
    + @endif +
    + +
    diff --git a/src/resources/views/livewire/admin/features/modals/form.blade.php b/src/resources/views/livewire/admin/features/modals/form.blade.php new file mode 100644 index 0000000..4d4beea --- /dev/null +++ b/src/resources/views/livewire/admin/features/modals/form.blade.php @@ -0,0 +1,160 @@ +
    + + {{-- HEADER --}} +
    + +
    + +
    + +
    +
    + {{ $featureId ? t('admin.features.edit') : t('admin.features.new') }} +
    + +
    + {{ t('admin.features.configure') }} +
    +
    + +
    + + + {{-- BODY --}} +
    + + {{-- NAME --}} +
    +
    {{ t('admin.features.name') }}
    + + +
    + + + {{-- KEY --}} +
    +
    {{ t('admin.features.key') }}
    + + +
    + + + {{-- ICON --}} +
    + +
    + {{ t('admin.features.icon') }} +
    + + @foreach($iconGroups as $group => $icons) + +
    + + {{-- GROUP TITLE --}} +
    + {{ $group }} +
    + +
    + + @foreach($icons as $i) + + @php + $selected = $icon === $i; + @endphp + + + + @endforeach + +
    + +
    + + @endforeach + +
    + + {{-- GROUP --}} +
    + +
    {{ t('admin.features.category') }}
    + + + +
    + + + {{-- STATUS --}} +
    + + {{ t('common.active') }} + + + +
    + +
    + + + {{-- FOOTER --}} +
    + + + + + +
    + +
    diff --git a/src/resources/views/livewire/admin/plans/index.blade.php b/src/resources/views/livewire/admin/plans/index.blade.php new file mode 100644 index 0000000..b9de8af --- /dev/null +++ b/src/resources/views/livewire/admin/plans/index.blade.php @@ -0,0 +1,199 @@ +@php + $publicPlans = $plans->where('is_internal', false); + $internalPlans = $plans->where('is_internal', true); +@endphp + +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('admin.plans.title') }}

    +

    {{ t('admin.plans.subtitle') }}

    +
    + +
    + + + {{-- ═══════════════════════════════════════════════════════ --}} + {{-- Γ–FFENTLICHE PLΓ„NE --}} + {{-- ═══════════════════════════════════════════════════════ --}} +
    +
    +

    {{ t('admin.plans.public') }}

    +
    + {{ $publicPlans->count() }} {{ t('admin.plans.plans') }} +
    + + @if($publicPlans->where('active', true)->isNotEmpty()) +
    + @foreach($publicPlans->where('active', true) as $plan) + @php + $aiConfig = is_string($plan->ai_config) ? json_decode($plan->ai_config, true) : ($plan->ai_config ?? []); + $model = $aiConfig['model'] ?? 'β€”'; + @endphp +
    + +
    +
    +

    {{ $plan->name }}

    +
    + {{ t('common.active') }} + @if($plan->is_featured) + {{ t('admin.plans.popular') }} + @endif +
    +
    +
    + + +
    +
    + +

    + @if($plan->price === 0) + {{ t('admin.plans.free') }} + @else + {{ number_format($plan->price / 100, 2, ',', '.') }} € + {{ t('admin.plans.per_month') }} + @endif +

    + +
    + + + {{ $plan->credit_limit <= 0 ? '∞' : number_format($plan->credit_limit) }} {{ t('common.credits') }} + + + + {{ $plan->device_limit <= 0 ? '∞' : $plan->device_limit }} {{ t('admin.plans.devices') }} + + + {{ $model }} + +
    + +
    + @endforeach +
    + @endif + + {{-- Inaktive ΓΆffentliche PlΓ€ne --}} + @if($publicPlans->where('active', false)->isNotEmpty()) +
    + @foreach($publicPlans->where('active', false) as $plan) +
    +
    +
    +

    {{ $plan->name }}

    + {{ t('common.inactive') }} +
    +
    + + +
    +
    +

    + {{ $plan->credit_limit <= 0 ? '∞' : number_format($plan->credit_limit) }} {{ t('common.credits') }} + · {{ $plan->device_limit <= 0 ? '∞' : $plan->device_limit }} {{ t('admin.plans.devices') }} +

    +
    + @endforeach +
    + @endif + + @if($publicPlans->isEmpty()) +

    {{ t('admin.plans.no_public') }}

    + @endif +
    + + + {{-- ═══════════════════════════════════════════════════════ --}} + {{-- INTERNE PLΓ„NE --}} + {{-- ═══════════════════════════════════════════════════════ --}} + @if($internalPlans->isNotEmpty()) +
    +
    +

    {{ t('admin.plans.internal') }}

    +
    + {{ t('admin.plans.not_public') }} + {{ $internalPlans->count() }} {{ t('admin.plans.plans') }} +
    + +
    + @foreach($internalPlans as $plan) + @php + $aiConfig = is_string($plan->ai_config) ? json_decode($plan->ai_config, true) : ($plan->ai_config ?? []); + $model = $aiConfig['model'] ?? 'β€”'; + $roleLabel = match($plan->plan_key ?? '') { + 'internal' => t('admin.plans.role.admin'), + 'developer' => 'Developer', + 'support' => 'Support', + 'affiliate' => 'Affiliate', + 'beta_tester' => 'Beta Tester', + default => null, + }; + @endphp +
    +
    +
    + +
    +
    +
    + {{ $plan->name }} + {{ t('admin.plans.internal_badge') }} + @if($roleLabel) + β†’ {{ $roleLabel }} + @endif +
    +
    + {{ $plan->credit_limit === 0 ? '∞ Unlimited' : number_format($plan->credit_limit) . ' Credits/mo' }} + · + {{ $model }} +
    +
    +
    +
    + {{ t('admin.plans.auto_assigned') }} + +
    +
    + @endforeach +
    +
    + @endif + + + {{-- EMPTY STATE --}} + @if($plans->isEmpty()) +
    +
    + +
    +

    {{ t('admin.plans.no_plans') }}

    +
    + @endif + +
    diff --git a/src/resources/views/livewire/admin/plans/modals/form.blade.php b/src/resources/views/livewire/admin/plans/modals/form.blade.php new file mode 100644 index 0000000..4e6debf --- /dev/null +++ b/src/resources/views/livewire/admin/plans/modals/form.blade.php @@ -0,0 +1,203 @@ +
    + + {{-- HEADER --}} +
    +
    +

    {{ $planId ? t('admin.plans.form.edit') : t('admin.plans.form.new') }}

    +

    {{ t('admin.plans.form.step', ['step' => $step]) }}

    +
    +
    + + +
    +
    + + + {{-- BODY --}} +
    + + {{-- STEP 1 --}} + @if($step === 1) + +
    + + +
    + +
    +
    + + + @if($price > 0) +

    = {{ number_format($price / 100, 2, ',', '.') }} €

    + @endif +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + {{-- AI Konfiguration --}} +
    +

    {{ t('admin.plans.form.ai_config') }}

    + +
    + + +
    + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    +

    {{ t('admin.plans.form.active') }}

    +

    {{ t('admin.plans.form.active_desc') }}

    +
    + +
    + +
    +
    +

    {{ t('admin.plans.form.featured') }}

    +

    {{ t('admin.plans.form.featured_desc') }}

    +
    + +
    + + @endif + + + {{-- STEP 2: FEATURES --}} + @if($step === 2) +
    + +
    +

    {{ t('admin.plans.form.assign') }}

    + +
    + + @foreach($groupedFeatures as $group) +
    +

    {{ $group->label }}

    +
    + @foreach($group->features as $feature) + @php $selected = in_array($feature->id, $selectedFeatures); @endphp + + @endforeach +
    +
    + @endforeach + +
    + @endif + +
    + + + {{-- FOOTER --}} +
    + @if($step === 2) + + @else +
    + @endif + +
    + + @if($step === 1) + + @else + + @endif +
    +
    + +
    diff --git a/src/resources/views/livewire/admin/translations/index.blade.php b/src/resources/views/livewire/admin/translations/index.blade.php new file mode 100644 index 0000000..176eb80 --- /dev/null +++ b/src/resources/views/livewire/admin/translations/index.blade.php @@ -0,0 +1,131 @@ +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('translations.title') }}

    +

    {{ t('translations.subtitle') }}

    +
    + +
    + + {{-- Suche --}} +
    + + +
    + + {{-- Nur fehlende --}} + + + {{-- Neu --}} + + +
    +
    + + + {{-- TABELLE --}} + @if(empty($grouped)) +
    +
    + +
    +

    {{ t('translations.empty') }}

    +
    + @else +
    + + + + + + @foreach(config('app.locales') as $code => $label) + + @endforeach + + + + + + @foreach($grouped as $group => $items) + + {{-- Gruppen-Header --}} + + + + + @foreach($items as $item) + + + {{-- KEY --}} + + + {{-- WERTE je Sprache --}} + @foreach(config('app.locales') as $code => $label) + @php $value = $item['values'][$code] ?? null; @endphp + + @endforeach + + {{-- AKTIONEN --}} + + + + @endforeach + + @endforeach + + +
    Key{{ $label }}
    + {{ $group }} +
    + {{ $item['key'] }} + + @if($value) + {{ \Illuminate\Support\Str::limit($value, 50) }} + @else + + + {{ t('translations.missing') }} + + @endif + +
    + + +
    +
    +
    + @endif + +
    diff --git a/src/resources/views/livewire/admin/translations/modals/delete-modal.blade.php b/src/resources/views/livewire/admin/translations/modals/delete-modal.blade.php new file mode 100644 index 0000000..f9745da --- /dev/null +++ b/src/resources/views/livewire/admin/translations/modals/delete-modal.blade.php @@ -0,0 +1,45 @@ +
    + + {{-- HEADER --}} +
    +
    + +
    +
    +

    {{ t('translations.delete_title') }}

    +

    {{ t('translations.delete_description') }}

    +
    +
    + + {{-- KEY INFO --}} +
    +

    {{ t('translations.key') }}

    + {{ $key }} +

    {{ $count }} {{ t('translations.entries') }}

    +
    + + {{-- BESTΓ„TIGUNG --}} +
    + + +
    + + {{-- FOOTER --}} +
    + + +
    + +
    diff --git a/src/resources/views/livewire/admin/translations/modals/edit-modal.blade.php b/src/resources/views/livewire/admin/translations/modals/edit-modal.blade.php new file mode 100644 index 0000000..9b13a8b --- /dev/null +++ b/src/resources/views/livewire/admin/translations/modals/edit-modal.blade.php @@ -0,0 +1,50 @@ +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('translations.edit_title') }}

    +

    {{ t('translations.edit_subtitle') }}

    +
    + +
    + + {{-- BODY --}} +
    + +
    + + +
    + + @foreach(config('app.locales') as $code => $label) +
    + + +
    + @endforeach + +
    + + {{-- FOOTER --}} +
    + + +
    + +
    diff --git a/src/resources/views/livewire/admin/users/calendar.blade.php b/src/resources/views/livewire/admin/users/calendar.blade.php new file mode 100644 index 0000000..63e855c --- /dev/null +++ b/src/resources/views/livewire/admin/users/calendar.blade.php @@ -0,0 +1,226 @@ +
    + + {{-- Back Link --}} + + + {{ t('admin.calendar.back_to') }} {{ $user->name }} + + + {{-- Header --}} +
    +

    {{ t('admin.calendar.title') }} {{ $user->name }}

    +

    {{ $events->count() }} {{ t('admin.calendar.events_total') }}

    +
    + + @if($events->isEmpty()) +
    + +

    {{ t('admin.calendar.no_events') }}

    +
    + @else + @php + $grouped = $events->groupBy(fn($e) => ($e->starts_at ?? $e->start_date)?->translatedFormat('F Y') ?? t('admin.calendar.no_date')); + @endphp + + @foreach($grouped as $month => $monthEvents) +
    + {{-- Monats-Header --}} +
    +

    {{ $month }}

    + {{ $monthEvents->count() }} +
    +
    + + @php + $byDay = $monthEvents->groupBy(fn($e) => ($e->starts_at ?? $e->start_date)?->format('Y-m-d') ?? 'none'); + @endphp + +
    + @foreach($byDay as $date => $dayEvents) + @php + $dateObj = $date !== 'none' ? \Carbon\Carbon::parse($date) : null; + @endphp + +
    + {{-- Datum-Spalte --}} +
    + @if($dateObj) +

    {{ $dateObj->translatedFormat('D') }}

    +

    {{ $dateObj->format('d') }}

    + @else +

    -

    + @endif +
    + + {{-- Events des Tages --}} +
    + @foreach($dayEvents as $event) + + @endforeach +
    +
    + @endforeach +
    +
    + @endforeach + @endif + + + {{-- EVENT DETAIL SIDEBAR --}} +
    + {{-- Backdrop --}} +
    + + {{-- Panel --}} +
    + @if($selectedEvent) + {{-- Header --}} +
    +
    +
    + +
    +
    +

    {{ t('admin.calendar.event_details') }}

    +

    {{ t('admin.calendar.read_only') }}

    +
    +
    + +
    + + {{-- Content --}} +
    + + {{-- Titel --}} +
    +
    + @if($selectedEvent->color) + + @endif +

    {{ $selectedEvent->title }}

    +
    +
    + + {{-- Datum & Zeit --}} +
    +
    + +
    +

    + {{ ($selectedEvent->starts_at ?? $selectedEvent->start_date)?->translatedFormat('l, d. F Y') ?? '-' }} +

    + @if($selectedEvent->ends_at && $selectedEvent->starts_at?->toDateString() !== $selectedEvent->ends_at->toDateString()) +

    + {{ t('common.until') }} {{ $selectedEvent->ends_at->translatedFormat('l, d. F Y') }} +

    + @endif +
    +
    + +
    + +

    + @if($selectedEvent->is_all_day) + {{ t('calendar.all_day_full') }} + @else + {{ $selectedEvent->starts_at?->format('H:i') }} + @if($selectedEvent->ends_at) + – {{ $selectedEvent->ends_at->format('H:i') }} + + ({{ $selectedEvent->starts_at->diff($selectedEvent->ends_at)->format('%hh %imin') }}) + + @endif + @endif +

    +
    +
    + + {{-- Notizen --}} + @if($selectedEvent->notes) +
    +

    {{ t('admin.calendar.notes') }}

    +
    +

    {{ $selectedEvent->notes }}

    +
    +
    + @endif + + {{-- Meta --}} +
    +

    {{ t('admin.calendar.details') }}

    +
    +
    + {{ t('admin.calendar.created') }} + {{ $selectedEvent->created_at?->translatedFormat('d. M Y, H:i') }} +
    +
    + {{ t('admin.calendar.updated') }} + {{ $selectedEvent->updated_at?->translatedFormat('d. M Y, H:i') }} +
    + @if($selectedEvent->google_event_id) +
    + {{ t('admin.calendar.google_sync') }} + {{ t('admin.calendar.connected') }} +
    + @endif +
    + ID + {{ Str::limit($selectedEvent->id, 12) }} +
    +
    +
    + +
    + @endif +
    +
    + +
    diff --git a/src/resources/views/livewire/admin/users/detail.blade.php b/src/resources/views/livewire/admin/users/detail.blade.php new file mode 100644 index 0000000..e3819d6 --- /dev/null +++ b/src/resources/views/livewire/admin/users/detail.blade.php @@ -0,0 +1,522 @@ +@php + $statusClasses = [ + 'success' => ['badge' => 'bg-green-50 text-green-700', 'dot' => 'bg-green-500'], + 'failed' => ['badge' => 'bg-red-50 text-red-700', 'dot' => 'bg-red-500'], + 'error' => ['badge' => 'bg-red-50 text-red-700', 'dot' => 'bg-red-500'], + 'conflict' => ['badge' => 'bg-amber-50 text-amber-700', 'dot' => 'bg-amber-400'], + 'duplicate' => ['badge' => 'bg-blue-50 text-blue-700', 'dot' => 'bg-blue-400'], + 'processing' => ['badge' => 'bg-purple-50 text-purple-700', 'dot' => 'bg-purple-400'], + ]; + + $typeBadges = [ + 'event' => ['#', t('agent.type.event'), '#4F46E5', '#EEF2FF'], + 'event_update' => ['✎', t('agent.type.event'), '#4F46E5', '#EEF2FF'], + 'note' => ['#', t('agent.type.note'), '#B45309', '#FFFBEB'], + 'note_update' => ['✎', t('agent.type.note'), '#B45309', '#FFFBEB'], + 'task' => ['#', t('agent.type.task'), '#065F46', '#ECFDF5'], + 'task_update' => ['✎', t('agent.type.task'), '#065F46', '#ECFDF5'], + 'contact' => ['#', t('agent.type.contact'), '#1E40AF', '#EFF6FF'], + 'email' => ['#', t('agent.type.email'), '#6B21A8', '#FAF5FF'], + 'chat' => ['#', 'Chat', '#374151', '#F3F4F6'], + 'multi' => ['#', t('agent.type.multi'), '#9D174D', '#FDF2F8'], + 'bonus' => ['#', t('agent.type.bonus'), '#065F46', '#ECFDF5'], + 'system' => ['#', t('agent.type.system'), '#374151', '#F9FAFB'], + ]; + + $statusLabels = [ + 'success' => t('agent.status.success'), + 'failed' => t('agent.status.failed'), + 'error' => t('agent.status.error'), + 'conflict' => t('agent.status.conflict'), + 'duplicate' => t('agent.status.duplicate'), + 'processing' => t('agent.status.processing'), + ]; +@endphp + +
    + + {{-- Back Link --}} + + + {{ t('admin.detail.back') }} + + + {{-- User Header --}} +
    +
    +
    +
    + {{ strtoupper(substr($user->name, 0, 1)) }} +
    +
    +

    {{ $user->name }}

    +

    {{ $user->email }}

    +
    + @if($user->subscription?->plan) + + {{ $user->subscription->plan->name }} + + @else + {{ t('admin.detail.free') }} + @endif + + @php + $statusBadge = match($user->status) { + \App\Enums\UserStatus::Active => 'bg-green-50 text-green-700', + \App\Enums\UserStatus::Blocked => 'bg-red-50 text-red-700', + \App\Enums\UserStatus::Suspended => 'bg-amber-50 text-amber-700', + default => 'bg-gray-100 text-gray-600', + }; + $statusLabel = match($user->status) { + \App\Enums\UserStatus::Active => t('admin.users.active'), + \App\Enums\UserStatus::Blocked => t('admin.users.blocked'), + \App\Enums\UserStatus::Suspended => t('admin.users.suspended'), + default => $user->status?->value ?? '-', + }; + @endphp + {{ $statusLabel }} + + + {{ $user->role->label() }} + +
    +
    +
    +
    + + + {{ t('admin.users.calendar') }} + + + {{-- Status Γ€ndern --}} +
    + +
    + @foreach(['active' => t('admin.users.active'), 'suspended' => t('admin.users.suspended'), 'blocked' => t('admin.users.blocked')] as $val => $label) + @if($user->status?->value !== $val) + + @endif + @endforeach +
    +
    +
    +
    +
    + + {{-- Stat Cards --}} +
    + @php + $statCards = [ + ['label' => t('admin.detail.events'), 'value' => $stats['events'], 'icon' => 'calendar', 'color' => 'indigo'], + ['label' => t('admin.detail.notes'), 'value' => $stats['notes'], 'icon' => 'document-text', 'color' => 'blue'], + ['label' => t('admin.detail.tasks'), 'value' => $stats['tasks'], 'icon' => 'check-circle', 'color' => 'green'], + ['label' => t('admin.detail.contacts'), 'value' => $stats['contacts'], 'icon' => 'users', 'color' => 'purple'], + ['label' => t('admin.detail.credits_month'), 'value' => number_format($stats['creditsUsed']), 'icon' => 'bolt', 'color' => 'amber'], + ['label' => t('admin.detail.total_costs'), 'value' => '$' . number_format($stats['totalCost'], 2), 'icon' => 'currency-dollar', 'color' => 'red'], + ]; + @endphp + + @foreach($statCards as $card) +
    +
    +
    + +
    +
    +

    {{ $card['value'] }}

    +

    {{ $card['label'] }}

    +
    + @endforeach +
    + + {{-- Additional Info --}} +
    +
    +

    {{ t('admin.detail.last_activity') }}

    +

    {{ $stats['lastActive'] ? \Carbon\Carbon::parse($stats['lastActive'])->diffForHumans() : t('admin.detail.never') }}

    +
    +
    +

    {{ t('admin.detail.credit_balance') }}

    +

    {{ number_format($user->credit_balance) }}

    +
    +
    + + {{-- Rolle Γ€ndern (nur Super Admin) --}} + @can('change-roles') +
    +
    + +

    {{ t('admin.detail.change_role') }}

    +
    +
    +
    + + +
    + +
    +
    + @endcan + + @if($user->isInternalUser()) + {{-- Interner Account Hinweis --}} +
    + +

    + {{ t('admin.detail.internal') }} ({{ $user->role->label() }}) + β€” {{ t('admin.detail.internal_desc') }} +

    +
    + @else + + {{-- Credit-Transaktionen --}} +
    +
    +
    + +

    {{ t('admin.detail.transactions') }}

    +
    + {{ number_format($user->credit_balance) }} {{ t('common.credits') }} +
    + + @if($creditTx->isEmpty()) +
    +

    {{ t('admin.detail.no_transactions') }}

    +
    + @else +
    + @foreach($creditTx as $tx) + @php + $txBadge = match($tx->type) { + 'onboarding' => ['Willkommen', 'bg-green-50 text-green-700'], + 'affiliate' => ['Affiliate', 'bg-indigo-50 text-indigo-700'], + 'admin_gift' => ['Geschenk', 'bg-purple-50 text-purple-700'], + 'refund' => ['RΓΌckerstattung', 'bg-green-50 text-green-700'], + 'subscription' => ['Abo-Bonus', 'bg-blue-50 text-blue-700'], + 'usage' => ['Verbrauch', 'bg-red-50 text-red-700'], + default => [$tx->type, 'bg-gray-100 text-gray-500'], + }; + @endphp +
    +
    +
    + {{ $txBadge[0] }} + {{ $tx->description }} +
    +
    + {{ $tx->created_at->format('d.m.Y H:i') }} + @if($tx->creator) + Β· {{ t('admin.detail.by') }} {{ $tx->creator->name }} + @endif +
    +
    + + {{ $tx->amount > 0 ? '+' : '' }}{{ number_format($tx->amount) }} + +
    + @endforeach +
    + @endif +
    + + {{-- Aktueller Zugang --}} +
    +

    {{ t('admin.detail.current_access') }}

    + + @if($activeSub) +
    +
    +
    + {{ $activeSub->plan?->name ?? $activeSub->plan_name }} + + {{ $activeSub->status->label() }} + +
    +

    + @if($activeSub->ends_at) + {{ t('admin.detail.expires') }} {{ $activeSub->ends_at->format('d.m.Y') }} + ({{ $activeSub->ends_at->diffForHumans() }}) + @else + {{ t('admin.detail.unlimited') }} + @endif +

    + @if($activeSub->gifted_by) +

    + + {{ t('admin.detail.gifted_by') }} {{ $activeSub->gifter?->name ?? t('admin.detail.admin') }} + am {{ $activeSub->gifted_at?->format('d.m.Y') }} + @if($activeSub->gift_reason) + Β· {{ $activeSub->gift_reason }} + @endif +

    + @endif +
    + + +
    + @else +

    {{ t('admin.detail.no_access') }}

    + @endif +
    + + {{-- Zugang schenken (nur Admin+) --}} + @can('gift-access') +
    +
    + +

    {{ t('admin.detail.gift_access') }}

    +
    +
    +
    +
    + + + @error('giftPlanId') +

    {{ $message }}

    + @enderror +
    +
    + + +
    +
    +
    + + +
    + +
    +
    + @endcan + + {{-- Give Credits Form (nur Admin+) --}} + @can('give-credits') +
    +

    {{ t('admin.detail.gift_credits') }}

    +
    +
    + + @error('creditAmount') +

    {{ $message }}

    + @enderror +
    +
    + + @error('creditReason') +

    {{ $message }}

    + @enderror +
    + +
    +
    + @endcan + + @endif {{-- Ende: !isInternalUser --}} + + {{-- Agent Logs --}} +
    +
    +

    {{ t('admin.detail.agent_logs') }}

    + {{ $logs->total() }} {{ t('admin.detail.entries') }} +
    + + @if($logs->isEmpty()) +
    +
    + +
    +

    {{ t('admin.no_logs') }}

    +
    + @else +
    + @foreach($logs as $log) + @php + $sc = $statusClasses[$log->status] ?? ['badge' => 'bg-gray-100 text-gray-500', 'dot' => 'bg-gray-400']; + $badge = $typeBadges[$log->type] ?? ['#', ucfirst($log->type ?? '?'), '#374151', '#F9FAFB']; + + $dur = match(true) { + $log->duration_ms === null => 'β€”', + $log->duration_ms >= 1000 => round($log->duration_ms / 1000, 1) . 's', + default => $log->duration_ms . 'ms', + }; + @endphp + +
    +
    + + {{-- Status Dot --}} +
    + +
    + + {{-- Content --}} +
    + + {{-- Input + Datum --}} +
    +

    {{ $log->input }}

    + + {{ $log->created_at?->format('d.m. H:i') }} + +
    + + {{-- Badges + Metriken --}} +
    + + {{-- Typ --}} + + + {{ $badge[1] }}{{ $badge[0] === '✎' ? ' ✎' : '' }} + + + {{-- Status --}} + + {{ $statusLabels[$log->status] ?? $log->status }} + + + | + + {{-- Credits --}} + + + {{ $log->credits }} Credits + + + {{-- Tokens --}} + @if($log->total_tokens > 0) + + + {{ number_format($log->total_tokens) }} + ({{ number_format($log->prompt_tokens) }}↑ {{ number_format($log->completion_tokens) }}↓) + + @endif + + {{-- Dauer --}} + + + {{ $dur }} + + + {{-- Kosten --}} + @if($log->cost_usd > 0) + ${{ number_format($log->cost_usd, 4) }} + @endif + + {{-- Model --}} + @if($log->model) + + {{ $log->model }} + + @endif +
    + + {{-- Fehler-Ausgabe --}} + @if(in_array($log->status, ['failed', 'error']) && !empty($log->output['error'])) +

    + + {{ $log->output['error'] }} +

    + @endif +
    + + {{-- Delete / Refund --}} + @if($log->credits > 0) +
    + +
    + @endif + +
    +
    + @endforeach +
    + + {{-- Pagination --}} + @if($logs->hasPages()) +
    + + {{ $logs->firstItem() }}–{{ $logs->lastItem() }} {{ t('admin.detail.of_entries', ['total' => $logs->total()]) }} + +
    + @if($logs->onFirstPage()) + β€Ή + @else + + @endif + + @foreach($logs->getUrlRange(max(1, $logs->currentPage() - 2), min($logs->lastPage(), $logs->currentPage() + 2)) as $page => $url) + + @endforeach + + @if($logs->hasMorePages()) + + @else + β€Ί + @endif +
    +
    + @endif + @endif +
    + +
    diff --git a/src/resources/views/livewire/admin/users/index.blade.php b/src/resources/views/livewire/admin/users/index.blade.php new file mode 100644 index 0000000..4be93f3 --- /dev/null +++ b/src/resources/views/livewire/admin/users/index.blade.php @@ -0,0 +1,206 @@ +
    + + {{-- Header --}} +
    +
    +

    {{ t('admin.users.title') }}

    +

    {{ $users->total() }} {{ t('admin.users.total') }}

    +
    +
    + + {{-- Tabs + Suche --}} +
    +
    + {{-- Tabs --}} + @php + $tabs = [ + 'all' => ['label' => t('admin.users.all'), 'icon' => null, 'color' => 'indigo'], + 'active' => ['label' => t('admin.users.active'), 'icon' => null, 'color' => 'indigo'], + 'suspended' => ['label' => t('admin.users.suspended'), 'icon' => null, 'color' => 'indigo'], + 'blocked' => ['label' => t('admin.users.blocked'), 'icon' => null, 'color' => 'indigo'], + 'staff' => ['label' => t('admin.users.team'), 'icon' => 'shield-check', 'color' => 'purple'], + ]; + @endphp +
    + @foreach($tabs as $key => $t) + @php + $active = $tab === $key; + $count = $counts[$key] ?? 0; + $color = $t['color']; + + $tabClasses = $active + ? "bg-{$color}-600 text-white border-{$color}-600" + : ($color === 'purple' + ? 'bg-white text-purple-500 border-purple-200 hover:border-purple-300 hover:text-purple-700' + : 'bg-white text-gray-500 border-gray-200 hover:border-gray-300 hover:text-gray-700'); + @endphp + + @endforeach +
    + + {{-- Suche --}} +
    + + +
    +
    +
    +
    + + {{-- Users Table --}} +
    +
    + + + + + + + + + + + + + + + @forelse($users as $user) + + + + + + + + + + + @empty + + + + @endforelse + +
    {{ t('admin.users.user') }}{{ t('admin.users.plan') }}{{ t('common.status') }}{{ t('admin.users.events') }}{{ t('admin.users.logs') }}{{ t('admin.users.registered') }}{{ t('admin.users.last_login') }}{{ t('common.actions') }}
    +
    + {{-- Avatar --}} +
    + {{ strtoupper(substr($user->name, 0, 1)) }} +
    +
    +
    + + {{ $user->name }} + + @if($user->role !== \App\Enums\UserRole::User) + + + {{ $user->role->label() }} + + @endif +
    +

    {{ $user->email }}

    +
    +
    +
    + @if($user->subscription?->plan) + + {{ $user->subscription->plan->name }} + + @else + Free + @endif + + @php + $statusBadge = match($user->status) { + \App\Enums\UserStatus::Active => 'bg-green-50 text-green-700', + \App\Enums\UserStatus::Blocked => 'bg-red-50 text-red-700', + \App\Enums\UserStatus::Suspended => 'bg-amber-50 text-amber-700', + default => 'bg-gray-100 text-gray-600', + }; + $statusLabel = match($user->status) { + \App\Enums\UserStatus::Active => t('admin.users.active'), + \App\Enums\UserStatus::Blocked => t('admin.users.blocked'), + \App\Enums\UserStatus::Suspended => t('admin.users.suspended'), + default => $user->status?->value ?? '-', + }; + @endphp + {{ $statusLabel }} + {{ $user->events_count }}{{ $user->agent_logs_count }}{{ $user->created_at?->format('d.m.Y') }}{{ $user->updated_at?->diffForHumans() }} +
    + + + + + + + + {{-- Status Dropdown --}} +
    + +
    + @foreach(['active' => t('admin.users.active'), 'suspended' => t('admin.users.suspended'), 'blocked' => t('admin.users.blocked')] as $val => $label) + @if($user->status?->value !== $val) + + @endif + @endforeach +
    +
    +
    +
    {{ t('admin.users.not_found') }}
    +
    + +
    + + {{-- Pagination --}} + @if($users->hasPages()) +
    + + {{ $users->firstItem() }}–{{ $users->lastItem() }} {{ t('admin.users.of_total', ['total' => $users->total()]) }} + +
    + @if($users->onFirstPage()) + β€Ή + @else + + @endif + + @foreach($users->getUrlRange(max(1, $users->currentPage() - 2), min($users->lastPage(), $users->currentPage() + 2)) as $page => $url) + + @endforeach + + @if($users->hasMorePages()) + + @else + β€Ί + @endif +
    +
    + @endif + +
    diff --git a/src/resources/views/livewire/admin/versions.blade.php b/src/resources/views/livewire/admin/versions.blade.php new file mode 100644 index 0000000..93b9761 --- /dev/null +++ b/src/resources/views/livewire/admin/versions.blade.php @@ -0,0 +1,133 @@ +
    + + {{-- Form --}} +
    +

    + {{ $editingId ? t('admin.versions.edit') : t('admin.versions.new') }} +

    + +
    +
    + + + @error('version')

    {{ $message }}

    @enderror +
    +
    + + + @error('name')

    {{ $message }}

    @enderror +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + +
    + + +
    + +
    + + @if($editingId) + + @endif +
    +
    + + {{-- Liste --}} +
    + + + + + + + + + + + + + @foreach($this->versions as $v) + + + + + + + + + @endforeach + +
    {{ t('admin.versions.version') }}{{ t('admin.versions.name') }}{{ t('admin.versions.status') }}{{ t('admin.versions.platform') }}{{ t('admin.versions.released') }}
    + {{ $v->version }} + {{ $v->name }} + + {{ ucfirst($v->status) }} + + {{ ucfirst($v->platform) }} + {{ $v->released_at?->format('d.m.Y H:i') ?? 'β€”' }} + +
    + @if($v->status !== 'live') + + @endif + + +
    +
    +
    + +
    diff --git a/src/resources/views/livewire/agent/history.blade.php b/src/resources/views/livewire/agent/history.blade.php new file mode 100644 index 0000000..7c09013 --- /dev/null +++ b/src/resources/views/livewire/agent/history.blade.php @@ -0,0 +1,70 @@ +@php + $statusDot = [ + 'success' => 'bg-green-500', + 'failed' => 'bg-red-500', + 'conflict' => 'bg-amber-400', + 'duplicate' => 'bg-blue-400', + 'processing' => 'bg-purple-400', + ]; + $typeBadges = [ + 'event' => ['#', t('agent.type.event'), '#4F46E5', '#EEF2FF'], + 'event_update' => ['✎', t('agent.type.event_update'), '#4F46E5', '#EEF2FF'], + 'note' => ['#', t('agent.type.note'), '#B45309', '#FFFBEB'], + 'note_update' => ['✎', t('agent.type.note_update'), '#B45309', '#FFFBEB'], + 'task' => ['#', t('agent.type.task'), '#065F46', '#ECFDF5'], + 'task_update' => ['✎', t('agent.type.task_update'), '#065F46', '#ECFDF5'], + 'contact' => ['#', t('agent.type.contact'), '#1E40AF', '#EFF6FF'], + 'email' => ['#', t('agent.type.email'), '#6B21A8', '#FAF5FF'], + 'chat' => ['#', t('agent.type.chat'), '#374151', '#F3F4F6'], + 'multi' => ['#', t('agent.type.multi'), '#9D174D', '#FDF2F8'], + ]; +@endphp + +
    + + @forelse($history as $item) + @php + $badge = $typeBadges[$item->type] ?? ['#', ucfirst($item->type ?? ''), '#374151', '#F9FAFB']; + @endphp +
    + + {{-- Status-Dot --}} + + +
    +
    + + + {{ $badge[1] }}{{ $badge[0] === '✎' ? ' ✎' : '' }} + +

    {{ $item->output['title'] ?? $item->input }}

    +
    +
    + + {{ $item->created_at->diffForHumans() }} + + @if($item->credits > 0) + + + {{ $item->credits }} + + @endif + @if($item->duration_ms) + + {{ $item->duration_ms >= 1000 ? round($item->duration_ms / 1000, 1) . 's' : $item->duration_ms . 'ms' }} + + @endif +
    +
    + +
    + @empty +
    +
    + +
    +

    {{ t('agent.no_requests') }}

    +
    + @endforelse + +
    diff --git a/src/resources/views/livewire/agent/index.blade.php b/src/resources/views/livewire/agent/index.blade.php new file mode 100644 index 0000000..6e5a4a8 --- /dev/null +++ b/src/resources/views/livewire/agent/index.blade.php @@ -0,0 +1,386 @@ +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('agent.title') }}

    +

    {{ t('agent.subtitle') }}

    +
    +
    + + + {{ t('agent.active') }} + + @if(count($conversation) > 0) + + @endif + + + {{ t('agent.history') }} + +
    +
    + + + {{-- USAGE --}} + @if(auth()->user()->isUnlimitedUser()) +
    +
    +
    + + {{ t('agent.credits') }} +
    + + + {{ t('agent.unlimited') }} + +
    +
    + @else + @php + $barColor = match(true) { + $usagePercent >= 100 => 'bg-red-500', + $usagePercent >= 80 => 'bg-amber-500', + $usagePercent >= 60 => 'bg-yellow-400', + default => 'bg-indigo-600', + }; + $creditBadge = match(true) { + $usagePercent >= 100 => [ + 'label' => t('agent.full'), + 'bg' => '#FEF2F2', + 'color' => '#991B1B', + 'dot' => '#EF4444', + ], + $usagePercent >= 80 => [ + 'label' => $usage . ' / ' . $limit, + 'bg' => '#FFFBEB', + 'color' => '#92400E', + 'dot' => '#F59E0B', + ], + $usagePercent >= 60 => [ + 'label' => $usage . ' / ' . $limit, + 'bg' => '#FFF7ED', + 'color' => '#9A3412', + 'dot' => '#FB923C', + ], + default => [ + 'label' => $usage . ' / ' . $limit, + 'bg' => '#EEF2FF', + 'color' => '#3730A3', + 'dot' => '#4F46E5', + ], + }; + @endphp +
    +
    +
    + + {{ t('agent.credits_month') }} +
    + + + {{ $creditBadge['label'] }} + +
    +
    +
    +
    + @php + $planLimit = auth()->user()->subscription?->plan?->credit_limit ?? 0; + $bonusLeft = auth()->user()->bonus_credits; + @endphp + @if($bonusLeft > 0) +

    + {{ number_format($planLimit) }} {{ t('agent.plan') }} + + {{ number_format($bonusLeft) }} {{ t('agent.balance') }} +

    + @endif + @if($limit > 0 && $usagePercent >= 80 && $usagePercent < 100) +

    {{ t('agent.almost_used') }} β€” {{ t('agent.upgrade_pro') }}

    + @elseif($limit > 0 && $usagePercent >= 100) +

    {{ t('agent.quota_exhausted') }} β€” {{ t('agent.upgrade_now') }}

    + @endif +
    + @endif + + + {{-- LAYOUT: CHAT (links) + VERLAUF (rechts) --}} +
    + + {{-- LEFT: CHAT --}} +
    + + {{-- VOICE ORB OVERLAY --}} +
    + + {{-- Close Button --}} + + + {{-- 3D Orb Container --}} +
    + + {{-- Bottom section --}} +
    + + {{-- Live Transcript --}} +
    +

    +

    {{ t('agent.ready') }}

    +

    {{ t('agent.speak_now') }}

    +

    {{ t('agent.thinking') }}

    +

    {{ t('agent.answering') }}

    +
    + + {{-- Stop Button --}} + +
    +
    + + + {{-- CHAT CARD (hidden when voice mode) --}} +
    + + {{-- CHAT MESSAGES --}} +
    + + {{-- Willkommen wenn leer --}} + @if(count($conversation) === 0) +
    +
    + +
    +

    {{ t('agent.greeting') }}

    +

    + {{ t('agent.intro') }} +

    + + {{-- Quick-Chips --}} +
    + + + +
    +
    + @endif + + {{-- Nachrichten --}} + @foreach($conversation as $msg) + @if($msg['role'] === 'user') +
    +
    +

    {{ $msg['content'] }}

    +
    +
    + @else +
    +
    + +
    +
    +

    {{ str_replace('[END]', '', $msg['content']) }}

    +
    +
    + @endif + @endforeach + + {{-- Optimistische User-Nachricht --}} + + + {{-- Typing Indicator --}} +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    + + {{-- AKTIONS-FEEDBACK --}} + @if($lastAction) + @php + $isSuccess = in_array($lastAction['status'], ['success', 'duplicate']); + $isFailed = $lastAction['status'] === 'failed'; + @endphp +
    + +
    + @if($isSuccess) + @elseif($isFailed) + @else + @endif +
    + +
    +

    + {{ $lastAction['message'] }} +

    + @if(!empty($lastAction['meta']['title']) || !empty($lastAction['meta']['date'])) +

    + @if(!empty($lastAction['meta']['title'])){{ $lastAction['meta']['title'] }}@endif + @if(!empty($lastAction['meta']['date'])) · {{ $lastAction['meta']['date'] }}@endif +

    + @endif +
    +
    + @endif + + {{-- INPUT --}} +
    +
    + + + {{-- Voice Button --}} + + + {{-- Send Button --}} + +
    +
    + +
    + +
    + + + {{-- RIGHT: VERLAUF --}} +
    + +
    +

    {{ t('agent.last_requests') }}

    + {{ t('agent.view_all') }} +
    + + + +
    + +
    + +
    diff --git a/src/resources/views/livewire/agent/logs.blade.php b/src/resources/views/livewire/agent/logs.blade.php new file mode 100644 index 0000000..8913b7e --- /dev/null +++ b/src/resources/views/livewire/agent/logs.blade.php @@ -0,0 +1,367 @@ +@php + $statusOptions = [ + '' => ['label' => t('agent.status.all'), 'color' => 'gray'], + 'success' => ['label' => t('agent.status.success'), 'color' => 'green'], + 'failed' => ['label' => t('agent.status.failed'), 'color' => 'red'], + 'conflict' => ['label' => t('agent.status.conflict'), 'color' => 'amber'], + 'duplicate' => ['label' => t('agent.status.duplicate'), 'color' => 'blue'], + 'processing' => ['label' => t('agent.status.processing'), 'color' => 'purple'], + ]; + + $statusClasses = [ + 'success' => ['badge' => 'bg-green-50 text-green-700', 'dot' => 'bg-green-500'], + 'failed' => ['badge' => 'bg-red-50 text-red-700', 'dot' => 'bg-red-500'], + 'conflict' => ['badge' => 'bg-amber-50 text-amber-700', 'dot' => 'bg-amber-400'], + 'duplicate' => ['badge' => 'bg-blue-50 text-blue-700', 'dot' => 'bg-blue-400'], + 'processing' => ['badge' => 'bg-purple-50 text-purple-700', 'dot' => 'bg-purple-400'], + ]; + + $typeBadges = [ + 'event' => ['#', t('agent.type.event'), '#4F46E5', '#EEF2FF'], + 'event_update' => ['✎', t('agent.type.event_update'), '#4F46E5', '#EEF2FF'], + 'note' => ['#', t('agent.type.note'), '#B45309', '#FFFBEB'], + 'note_update' => ['✎', t('agent.type.note_update'), '#B45309', '#FFFBEB'], + 'task' => ['#', t('agent.type.task'), '#065F46', '#ECFDF5'], + 'task_update' => ['✎', t('agent.type.task_update'), '#065F46', '#ECFDF5'], + 'contact' => ['#', t('agent.type.contact'), '#1E40AF', '#EFF6FF'], + 'email' => ['#', t('agent.type.email'), '#6B21A8', '#FAF5FF'], + 'chat' => ['#', t('agent.type.chat'), '#374151', '#F3F4F6'], + 'multi' => ['#', t('agent.type.multi'), '#9D174D', '#FDF2F8'], + 'bonus' => ['#', t('agent.type.bonus'), '#065F46', '#ECFDF5'], + 'system' => ['#', t('agent.type.system'), '#374151', '#F9FAFB'], + ]; +@endphp + +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('agent.logs.title') }}

    +

    + @if(auth()->user()->isDeveloper()) + {{ t('agent.logs.subtitle_admin') }} + @else + {{ t('agent.logs.subtitle') }} + @endif +

    +
    + + + {{ t('agent.logs.go_assistant') }} + +
    + + + {{-- STATS --}} + @if($stats && $stats->total > 0) + @php + $successRate = $stats->total > 0 + ? round(($stats->successes / $stats->total) * 100) + : 0; + $avgDurationSec = $stats->avg_duration + ? ($stats->avg_duration >= 1000 + ? round($stats->avg_duration / 1000, 1) . 's' + : round($stats->avg_duration) . 'ms') + : 'β€”'; + @endphp + {{-- Credit-Card mit Fortschritt --}} + @if(auth()->user()->isUnlimitedUser()) +
    +
    +
    +
    + +
    + {{ t('agent.logs.credits_month') }} +
    + + + {{ t('agent.unlimited') }} + +
    +
    + @else + @php + $roleDefault = match(auth()->user()->role) { + \App\Enums\UserRole::Admin => ['bg' => '#FAF5FF', 'color' => '#7E22CE', 'dot' => '#9333EA', 'bar' => 'bg-purple-600'], + \App\Enums\UserRole::Developer => ['bg' => '#F0FDF4', 'color' => '#15803D', 'dot' => '#22C55E', 'bar' => 'bg-green-600'], + \App\Enums\UserRole::Support => ['bg' => '#EFF6FF', 'color' => '#1D4ED8', 'dot' => '#3B82F6', 'bar' => 'bg-blue-500'], + \App\Enums\UserRole::Affiliate => ['bg' => '#FDF4FF', 'color' => '#A21CAF', 'dot' => '#D946EF', 'bar' => 'bg-fuchsia-500'], + \App\Enums\UserRole::BetaTester => ['bg' => '#FFF7ED', 'color' => '#C2410C', 'dot' => '#F97316', 'bar' => 'bg-orange-500'], + default => ['bg' => '#EEF2FF', 'color' => '#3730A3', 'dot' => '#4F46E5', 'bar' => 'bg-indigo-600'], + }; + + $creditBadge = match(true) { + $usagePercent >= 100 => ['label' => t('agent.full'), 'bg' => '#FEF2F2', 'color' => '#991B1B', 'dot' => '#EF4444', 'bar' => 'bg-red-500'], + $usagePercent >= 80 => ['label' => $usage . ' / ' . $limit, 'bg' => '#FFFBEB', 'color' => '#92400E', 'dot' => '#F59E0B', 'bar' => 'bg-amber-500'], + $usagePercent >= 60 => ['label' => $usage . ' / ' . $limit, 'bg' => '#FFF7ED', 'color' => '#9A3412', 'dot' => '#FB923C', 'bar' => 'bg-orange-400'], + default => array_merge($roleDefault, ['label' => $usage . ' / ' . $limit]), + }; + @endphp +
    +
    +
    +
    + +
    + {{ t('agent.logs.credits_month') }} +
    + + + {{ $creditBadge['label'] }} + +
    +
    +
    +
    +
    + {{ $usagePercent }}% {{ t('agent.logs.used') }} +
    +
    + @endif + + {{-- Übrige Stat-Cards --}} + @php + $statCards = [ + ['label' => t('agent.logs.requests_month'), 'value' => number_format($stats->total), 'sub' => null, 'icon' => 'cpu-chip', 'color' => 'text-indigo-600', 'bg' => 'bg-indigo-50'], + ['label' => t('agent.logs.success_rate'), 'value' => $successRate . '%', 'sub' => $stats->failures . ' ' . t('agent.logs.errors'), 'icon' => 'check-circle', 'color' => 'text-green-600', 'bg' => 'bg-green-50'], + ]; + if (auth()->user()->isDeveloper()) { + $statCards[] = ['label' => t('agent.logs.avg_response'), 'value' => $avgDurationSec, 'sub' => number_format($stats->total_tokens ?? 0) . ' Tokens', 'icon' => 'clock', 'color' => 'text-blue-600', 'bg' => 'bg-blue-50']; + } + @endphp +
    + @foreach($statCards as $stat) +
    +
    + @if($stat['icon'] === 'cpu-chip') + @elseif($stat['icon'] === 'check-circle') + @else + @endif +
    +
    +

    {{ $stat['value'] }}

    +

    {{ $stat['sub'] ?? $stat['label'] }}

    +
    +
    + @endforeach +
    + @endif + + + {{-- FILTER + SUCHE --}} +
    + + {{-- Suche --}} +
    + + +
    + + {{-- Status-Filter --}} +
    + @foreach($statusOptions as $key => $opt) + @php + $active = $filterStatus === $key; + $colorMap = [ + 'green' => $active ? 'bg-green-600 text-white border-green-600' : 'bg-white text-gray-500 border-gray-200 hover:border-green-300 hover:text-green-700', + 'red' => $active ? 'bg-red-600 text-white border-red-600' : 'bg-white text-gray-500 border-gray-200 hover:border-red-300 hover:text-red-700', + 'amber' => $active ? 'bg-amber-500 text-white border-amber-500' : 'bg-white text-gray-500 border-gray-200 hover:border-amber-300 hover:text-amber-700', + 'blue' => $active ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-500 border-gray-200 hover:border-blue-300 hover:text-blue-700', + 'purple' => $active ? 'bg-purple-600 text-white border-purple-600': 'bg-white text-gray-500 border-gray-200 hover:border-purple-300 hover:text-purple-700', + 'gray' => $active ? 'bg-indigo-600 text-white border-indigo-600': 'bg-white text-gray-500 border-gray-200 hover:border-gray-300 hover:text-gray-700', + ]; + @endphp + + @endforeach +
    + +
    + + + {{-- LOGS LISTE --}} + @if($logs->isEmpty()) + +
    +
    + +
    +

    {{ t('agent.logs.no_entries') }}

    + @if($search || $filterStatus) + + @else +

    {{ t('agent.logs.no_entries_hint') }}

    + @endif +
    + + @else + +
    + @foreach($logs as $log) + @php + $sc = $statusClasses[$log->status] ?? ['badge' => 'bg-gray-100 text-gray-500', 'dot' => 'bg-gray-400']; + $badge = $typeBadges[$log->type] ?? ['#', ucfirst($log->type ?? '?'), '#374151', '#F9FAFB']; + + // Dauer formatieren + if ($log->duration_ms !== null) { + $dur = $log->duration_ms >= 1000 + ? round($log->duration_ms / 1000, 1) . 's' + : $log->duration_ms . 'ms'; + } else { + $dur = 'β€”'; + } + + // Kosten formatieren + $cost = $log->cost_usd > 0 + ? '$' . number_format($log->cost_usd, 4) + : 'β€”'; + @endphp + +
    + +
    + + {{-- Status-Dot --}} +
    + +
    + + {{-- Inhalt --}} +
    + + {{-- Eingabe + Zeit --}} +
    +

    {{ $log->output['title'] ?? $log->input }}

    + + {{ $log->created_at->setTimezone($tz)->format('d.m. H:i') }} + +
    + + {{-- Badges + Metriken --}} +
    + + {{-- Typ --}} + + + {{ $badge[1] }}{{ $badge[0] === '✎' ? ' ✎' : '' }} + + + {{-- Status --}} + + {{ $statusOptions[$log->status]['label'] ?? $log->status }} + + + {{-- Trennstrich --}} + | + + {{-- Credits (alle User) --}} + + + {{ $log->credits }} Credits + + + {{-- Zeitstempel relativ (alle User) --}} + + + {{ $log->created_at->diffForHumans() }} + + + {{-- Admin-only: Tokens, Dauer, Kosten, Modell --}} + @if(auth()->user()->isDeveloper()) + + | + + {{-- Tokens --}} + @if($log->total_tokens > 0) + + + {{ number_format($log->total_tokens) }} Tokens + ({{ number_format($log->prompt_tokens) }}↑ {{ number_format($log->completion_tokens) }}↓) + + @endif + + {{-- Dauer --}} + + + {{ $dur }} + + + {{-- Kosten --}} + @if($log->cost_usd > 0) + {{ $cost }} + @endif + + {{-- Modell --}} + @if($log->model) + + {{ $log->model }} + + @endif + + @endif + +
    + + {{-- Fehler-Ausgabe bei failed --}} + @if($log->status === 'failed' && !empty($log->output['error'])) +

    + + {{ $log->output['error'] }} +

    + @endif + +
    + +
    + +
    + @endforeach +
    + + {{-- PAGINATION --}} + @if($logs->hasPages()) +
    + + {{ $logs->firstItem() }}–{{ $logs->lastItem() }} {{ t('common.of') }} {{ $logs->total() }} {{ t('common.entries') }} + +
    + @if($logs->onFirstPage()) + β€Ή + @else + + @endif + + @foreach($logs->getUrlRange(max(1, $logs->currentPage() - 2), min($logs->lastPage(), $logs->currentPage() + 2)) as $page => $url) + + @endforeach + + @if($logs->hasMorePages()) + + @else + β€Ί + @endif +
    +
    + @endif + + @endif + +
    diff --git a/src/resources/views/livewire/agent/modals/conflict-modal.blade.php b/src/resources/views/livewire/agent/modals/conflict-modal.blade.php new file mode 100644 index 0000000..2ba9ae3 --- /dev/null +++ b/src/resources/views/livewire/agent/modals/conflict-modal.blade.php @@ -0,0 +1,126 @@ +
    + + {{-- HEADER --}} +
    +
    + ⚠ +
    + +
    +
    + {{ t('agent.conflict.title') }} +
    + +
    + {{ t('agent.conflict.subtitle') }} +
    +
    +
    + + + {{-- BODY --}} +
    + + {{-- DEIN TERMIN --}} +
    +
    {{ t('agent.conflict.your_event') }}
    + +
    + {{ \Carbon\Carbon::parse($meta['start'])->format('d.m.Y H:i') }} + - + {{ \Carbon\Carbon::parse($meta['end'])->format('H:i') }} +
    +
    + + + {{-- FRÜHER --}} + @if(!empty($meta['suggestion_prev'])) +
    + +
    + {{ t('agent.conflict.earlier') }} +
    + +
    + {{ \Carbon\Carbon::parse($meta['suggestion_prev'])->format('d.m.Y H:i') }} +
    + +
    + @endif + + {{-- SPΓ„TER --}} + @if(!empty($meta['suggestion_next'])) +
    + +
    + {{ t('agent.conflict.later') }} +
    + +
    + {{ \Carbon\Carbon::parse($meta['suggestion_next'])->format('d.m.Y H:i') }} +
    + +
    + @endif + + + {{-- INFO --}} +
    + {{ t('agent.conflict.hint') }} +
    + +
    + + + {{-- FOOTER --}} +
    + +
    + + {{-- CANCEL --}} + + {{-- DYNAMISCHER BUTTON --}} + +
    + +
    + +
    diff --git a/src/resources/views/livewire/auth/forgot-password.blade.php b/src/resources/views/livewire/auth/forgot-password.blade.php new file mode 100644 index 0000000..fd2ec78 --- /dev/null +++ b/src/resources/views/livewire/auth/forgot-password.blade.php @@ -0,0 +1,83 @@ +
    + + {{-- LEFT SIDE (BRANDING) --}} + + + {{-- RIGHT SIDE --}} +
    +
    + +
    + +
    + +
    + + @if($sent) +
    +
    + +
    +

    E-Mail gesendet

    +

    + Falls ein Konto mit dieser Adresse existiert, erhΓ€ltst du eine E-Mail mit einem Reset-Link. +

    + + ← ZurΓΌck zum Login + +
    + @else +
    + + ZurΓΌck zum Login + +

    Passwort vergessen?

    +

    + Gib deine E-Mail ein und wir senden dir einen Reset-Link. +

    +
    + +
    +
    + + @error('email') +

    {{ $message }}

    + @enderror +
    + + +
    + @endif + +
    +
    +
    + +
    diff --git a/src/resources/views/livewire/auth/login.blade.php b/src/resources/views/livewire/auth/login.blade.php new file mode 100644 index 0000000..e78f282 --- /dev/null +++ b/src/resources/views/livewire/auth/login.blade.php @@ -0,0 +1,117 @@ +
    + + {{-- LEFT SIDE (BRANDING) --}} + + + + {{-- RIGHT SIDE (LOGIN) --}} +
    + +
    + {{-- MOBILE LOGO --}} +
    + +
    + + {{-- CARD --}} +
    + +
    +

    {{ t('auth.login.title') }}

    +

    {{ t('auth.login.subtitle') }}

    +
    + + @if(session()->has('success')) +
    + {{ session('success') }} +
    + @endif + +
    + + {{-- EMAIL --}} +
    + + +
    + @error('email') +

    {{ $message }}

    + @enderror +
    +
    + + {{-- PASSWORD --}} +
    + + +
    + @error('password') +

    {{ $message }}

    + @enderror +
    +
    + + {{-- BUTTON --}} + + +
    + + + +
    + +
    + +
    + +
    diff --git a/src/resources/views/livewire/auth/modals/verify-modal.blade.php b/src/resources/views/livewire/auth/modals/verify-modal.blade.php new file mode 100644 index 0000000..6f7a7a0 --- /dev/null +++ b/src/resources/views/livewire/auth/modals/verify-modal.blade.php @@ -0,0 +1,28 @@ +
    + +

    + {{ t('auth.enter_code') }} +

    + + + + @error('code') +
    {{ $message }}
    + @enderror + + + + + +
    diff --git a/src/resources/views/livewire/auth/register.blade.php b/src/resources/views/livewire/auth/register.blade.php new file mode 100644 index 0000000..ee3f731 --- /dev/null +++ b/src/resources/views/livewire/auth/register.blade.php @@ -0,0 +1,132 @@ +
    + + {{-- LEFT SIDE (BRANDING) --}} + + + + {{-- RIGHT SIDE (REGISTER) --}} +
    + +
    + + {{-- MOBILE LOGO --}} +
    + +
    + + {{-- CARD --}} +
    + +
    +

    {{ t('auth.register.title') }}

    +

    {{ t('auth.register.subtitle') }}

    +
    + +
    +
    + +
    + + {{-- NAME --}} +
    + + +
    + @error('name') +

    {{ $message }}

    + @enderror +
    +
    + + {{-- EMAIL --}} +
    + + +
    + @error('email') +

    {{ $message }}

    + @enderror +
    +
    + + {{-- PASSWORD --}} +
    + + +
    + @error('password') +

    {{ $message }}

    + @enderror +
    +
    + + {{-- BUTTON --}} + + +
    + + + +
    + +
    + +
    + +
    diff --git a/src/resources/views/livewire/auth/reset-password.blade.php b/src/resources/views/livewire/auth/reset-password.blade.php new file mode 100644 index 0000000..71828c6 --- /dev/null +++ b/src/resources/views/livewire/auth/reset-password.blade.php @@ -0,0 +1,82 @@ +
    + + {{-- LEFT SIDE (BRANDING) --}} + + + {{-- RIGHT SIDE --}} +
    +
    + +
    + +
    + +
    + +
    +

    Neues Passwort setzen

    +

    Gib dein neues Passwort ein.

    +
    + + @if($error) +
    + {{ $error }} + Neu anfordern β†’ +
    + @endif + +
    +
    + + + @error('password') +

    {{ $message }}

    + @enderror +
    + +
    + + +
    + + +
    + +
    +
    +
    + +
    diff --git a/src/resources/views/livewire/auth/verify-notice.blade.php b/src/resources/views/livewire/auth/verify-notice.blade.php new file mode 100644 index 0000000..cbff35a --- /dev/null +++ b/src/resources/views/livewire/auth/verify-notice.blade.php @@ -0,0 +1,94 @@ +
    + + {{-- LEFT SIDE (wie Login) --}} + + + + {{-- RIGHT SIDE --}} +
    + +
    + + {{-- ERROR MESSAGE --}} + @if(session('error.auth.verify.link_expired')) +
    + {{ session('error.auth.verify.link_expired') }} +
    + @endif + + {{-- CARD --}} +
    + + {{-- ICON --}} +
    +
    + +
    +
    + + {{-- TEXT --}} +
    +

    + {{ t('auth.verify.title') }} +

    + +

    + {{ t('auth.verify.subtitle') }} +

    +
    + + {{-- ACTIONS --}} +
    0 || $blockedUntil ) wire:poll.1s="tick" @endif class="space-y-3"> + + +

    + {{ t('auth.verify.hint') }} +

    + +
    +
    +
    + +
    + +
    diff --git a/src/resources/views/livewire/auth/verify.blade.php b/src/resources/views/livewire/auth/verify.blade.php new file mode 100644 index 0000000..eb756b2 --- /dev/null +++ b/src/resources/views/livewire/auth/verify.blade.php @@ -0,0 +1,184 @@ +
    + + {{-- LEFT SIDE (BRANDING) --}} + + + + {{-- RIGHT SIDE (VERIFY) --}} +
    + +
    + + {{-- MOBILE LOGO --}} +
    + +
    + + {{-- CARD --}} +
    + + {{-- HEADER --}} +
    +

    + {{ t('auth.verify.title') }} +

    + +

    + {{ $this->user->email }} +

    +
    + + {{-- CODE INPUTS --}} +
    + + @for ($i = 0; $i < 6; $i++) + + @endfor + +
    + + {{-- ERROR --}} +
    + @error('code') +

    {{ $message }}

    + @enderror +
    + + {{-- BUTTON --}} + + + {{-- RESEND --}} +
    0) + wire:poll.1s="tick" + @endif + class="text-center text-sm" + > + + @if($cooldown > 0) + {{ t('auth.verify.resend_wait') }} ({{ $cooldown }}s) + @else + + @endif + +
    +
    + +
    + +
    + +
    + + + diff --git a/src/resources/views/livewire/automation/index.blade.php b/src/resources/views/livewire/automation/index.blade.php new file mode 100644 index 0000000..14486ca --- /dev/null +++ b/src/resources/views/livewire/automation/index.blade.php @@ -0,0 +1,649 @@ +@php + $colorMap = [ + 'indigo' => ['bg' => 'bg-indigo-50', 'text' => 'text-indigo-600', 'border' => 'border-indigo-200', 'ring' => 'ring-indigo-500'], + 'amber' => ['bg' => 'bg-amber-50', 'text' => 'text-amber-600', 'border' => 'border-amber-200', 'ring' => 'ring-amber-500'], + 'blue' => ['bg' => 'bg-blue-50', 'text' => 'text-blue-600', 'border' => 'border-blue-200', 'ring' => 'ring-blue-500'], + 'green' => ['bg' => 'bg-green-50', 'text' => 'text-green-600', 'border' => 'border-green-200', 'ring' => 'ring-green-500'], + 'pink' => ['bg' => 'bg-pink-50', 'text' => 'text-pink-600', 'border' => 'border-pink-200', 'ring' => 'ring-pink-500'], + 'gray' => ['bg' => 'bg-gray-100', 'text' => 'text-gray-500', 'border' => 'border-gray-200', 'ring' => 'ring-gray-400'], + 'violet' => ['bg' => 'bg-violet-50', 'text' => 'text-violet-600', 'border' => 'border-violet-200', 'ring' => 'ring-violet-500'], + 'orange' => ['bg' => 'bg-orange-50', 'text' => 'text-orange-600', 'border' => 'border-orange-200', 'ring' => 'ring-orange-500'], + 'teal' => ['bg' => 'bg-teal-50', 'text' => 'text-teal-600', 'border' => 'border-teal-200', 'ring' => 'ring-teal-500'], + ]; + + $dayLabels = [1 => t('automations.day.monday'), 2 => t('automations.day.tuesday'), 3 => t('automations.day.wednesday'), 4 => t('automations.day.thursday'), 5 => t('automations.day.friday'), 6 => t('automations.day.saturday'), 7 => t('automations.day.sunday')]; +@endphp + +
    + + {{-- HEADER --}} +
    +

    {{ t('automations.title') }}

    +

    {{ t('automations.subtitle') }}

    +
    + + {{-- UPGRADE BANNER β€” nur fΓΌr Free User --}} + @if(!$isPro) +
    +
    +
    + +
    +
    +

    2 Automationen kostenlos verfΓΌgbar Β· Upgrade fΓΌr alle {{ count($types) }}

    +

    {{ t('automations.upgrade_hint', ['count' => count($types)]) }}

    +
    +
    + + {{ t('automations.upgrade_now') }} + + +
    + @endif + + {{-- KOSTENLOS --}} +
    +

    {{ t('automations.section.free') }}

    +
    + @foreach($types as $type => $def) + @if(!$def['is_free']) @continue @endif + @php + $record = $records[$type] ?? null; + $active = $record?->active ?? false; + $c = $colorMap[$def['color']]; + @endphp +
    + + {{-- TOP --}} +
    +
    + + {{-- Icon --}} +
    + @if($def['icon'] === 'bell') + + @elseif($def['icon'] === 'sun') + + @elseif($def['icon'] === 'calendar-days') + + @elseif($def['icon'] === 'clipboard-document-check') + + @elseif($def['icon'] === 'cake') + + @elseif($def['icon'] === 'clock') + + @elseif($def['icon'] === 'moon') + + @elseif($def['icon'] === 'user-group') + + @elseif($def['icon'] === 'calendar') + + @endif +
    + + {{-- Toggle --}} + + +
    + +
    +

    {{ t("automations.type.{$type}.name") }}

    +

    {{ t("automations.type.{$type}.description") }}

    +
    + + {{-- Aktuelle Konfiguration --}} + @if($record) +
    + @if($type === 'event_reminder') + + {{ $record->cfg('minutes_before', 30) >= 60 + ? ($record->cfg('minutes_before') / 60) . ' ' . t('automations.hours_before') + : $record->cfg('minutes_before', 30) . ' ' . t('automations.minutes_before') }} + + @elseif($type === 'daily_agenda') + + {{ $record->cfg('send_time', '08:00') }} {{ t('automations.oclock') }} + + @elseif($type === 'weekly_overview') + + {{ $dayLabels[$record->cfg('send_day', 1)] ?? t('automations.day.monday') }}, {{ $record->cfg('send_time', '08:00') }} {{ t('automations.oclock') }} + + @elseif($type === 'event_followup') + + {{ $record->cfg('delay_minutes', 30) }} {{ t('automations.min_after') }} + + @elseif($type === 'birthday_reminder') + + {{ $record->cfg('days_before', 7) == 0 + ? t('automations.before.today') + : $record->cfg('days_before', 7) . ' ' . t('automations.days_before') }} + + @elseif($type === 'no_activity_reminder') + + {{ t('automations.after_free_days') }} {{ $record->cfg('days_inactive', 3) }} + + @elseif($type === 'daily_summary') + + {{ $record->cfg('send_time', '18:00') }} {{ t('automations.oclock') }} + + @elseif($type === 'contact_followup') + + {{ t('automations.after_days') }} {{ $record->cfg('days_since_contact', 14) }} + + @elseif($type === 'free_slots_report') + + {{ $dayLabels[$record->cfg('send_day', 7)] ?? t('automations.day.sunday') }}, {{ $record->cfg('send_time', '17:00') }} {{ t('automations.oclock') }} + + + {{ t('automations.from_min') }} {{ $record->cfg('min_slot_minutes', 60) }} + + @endif + + @foreach($def['channels'] ?? ['push'] as $ch) + + {{ $ch === 'push' ? 'Push' : 'E-Mail' }} + + @endforeach + + @if($record->last_run_at) + + {{ t('automations.last_run') }} {{ $record->last_run_at->diffForHumans() }} + + @endif +
    + @endif +
    + + {{-- FOOTER --}} +
    + + + {{ $active ? t('common.active') : t('common.inactive') }} + + +
    + @if($record) + + @endif + +
    + +
    + +
    + @endforeach +
    +
    + + {{-- PRO --}} +
    +

    PRO

    +
    + @foreach($types as $type => $def) + @if($def['is_free']) @continue @endif + @php + $record = $records[$type] ?? null; + $active = $record?->active ?? false; + $c = $colorMap[$def['color']]; + @endphp + + @if(!$isPro) + {{-- Gesperrte Pro Karte fΓΌr Free User --}} + +
    +
    + @if($def['icon'] === 'bell') + @elseif($def['icon'] === 'sun') + @elseif($def['icon'] === 'calendar-days') + @elseif($def['icon'] === 'clipboard-document-check') + @elseif($def['icon'] === 'cake') + @elseif($def['icon'] === 'clock') + @elseif($def['icon'] === 'moon') + @elseif($def['icon'] === 'user-group') + @elseif($def['icon'] === 'calendar') + @endif +
    +
    +
    + + Pro +
    +
    +
    +
    +
    +
    +
    +
    +

    {{ t("automations.type.{$type}.name") }}

    +

    {{ t("automations.type.{$type}.description") }}

    +
    +
    + {{ t('common.inactive') }} + Freischalten +
    +
    + @else + {{-- Aktive Pro Karte --}} +
    + + {{-- TOP --}} +
    +
    + + {{-- Icon --}} +
    + @if($def['icon'] === 'bell') + + @elseif($def['icon'] === 'sun') + + @elseif($def['icon'] === 'calendar-days') + + @elseif($def['icon'] === 'clipboard-document-check') + + @elseif($def['icon'] === 'cake') + + @elseif($def['icon'] === 'clock') + + @elseif($def['icon'] === 'moon') + + @elseif($def['icon'] === 'user-group') + + @elseif($def['icon'] === 'calendar') + + @endif +
    + + {{-- Toggle --}} + + +
    + +
    +

    {{ t("automations.type.{$type}.name") }}

    +

    {{ t("automations.type.{$type}.description") }}

    +
    + + {{-- Aktuelle Konfiguration --}} + @if($record) +
    + @if($type === 'event_reminder') + + {{ $record->cfg('minutes_before', 30) >= 60 + ? ($record->cfg('minutes_before') / 60) . ' ' . t('automations.hours_before') + : $record->cfg('minutes_before', 30) . ' ' . t('automations.minutes_before') }} + + @elseif($type === 'daily_agenda') + + {{ $record->cfg('send_time', '08:00') }} {{ t('automations.oclock') }} + + @elseif($type === 'weekly_overview') + + {{ $dayLabels[$record->cfg('send_day', 1)] ?? t('automations.day.monday') }}, {{ $record->cfg('send_time', '08:00') }} {{ t('automations.oclock') }} + + @elseif($type === 'event_followup') + + {{ $record->cfg('delay_minutes', 30) }} {{ t('automations.min_after') }} + + @elseif($type === 'birthday_reminder') + + {{ $record->cfg('days_before', 7) == 0 + ? t('automations.before.today') + : $record->cfg('days_before', 7) . ' ' . t('automations.days_before') }} + + @elseif($type === 'no_activity_reminder') + + {{ t('automations.after_free_days') }} {{ $record->cfg('days_inactive', 3) }} + + @elseif($type === 'daily_summary') + + {{ $record->cfg('send_time', '18:00') }} {{ t('automations.oclock') }} + + @elseif($type === 'contact_followup') + + {{ t('automations.after_days') }} {{ $record->cfg('days_since_contact', 14) }} + + @elseif($type === 'free_slots_report') + + {{ $dayLabels[$record->cfg('send_day', 7)] ?? t('automations.day.sunday') }}, {{ $record->cfg('send_time', '17:00') }} {{ t('automations.oclock') }} + + + {{ t('automations.from_min') }} {{ $record->cfg('min_slot_minutes', 60) }} + + @endif + + @foreach($def['channels'] ?? ['push'] as $ch) + + {{ $ch === 'push' ? 'Push' : 'E-Mail' }} + + @endforeach + + @if($record->last_run_at) + + {{ t('automations.last_run') }} {{ $record->last_run_at->diffForHumans() }} + + @endif +
    + @endif +
    + + {{-- FOOTER --}} +
    + + + {{ $active ? t('common.active') : t('common.inactive') }} + + +
    + @if($record) + + @endif + +
    + +
    + +
    + @endif {{-- Ende: isPro --}} + @endforeach +
    +
    + + + {{-- ══════════════════════════════════════════════════════════════════ --}} + {{-- SLIDE-IN PANEL --}} + {{-- ══════════════════════════════════════════════════════════════════ --}} + + {{-- Backdrop --}} +
    + + {{-- Panel --}} +
    + + @if($activeType && isset($types[$activeType])) + @php + $def = $types[$activeType]; + $c = $colorMap[$def['color']]; + @endphp + + {{-- Panel Header --}} +
    +
    +
    + @if($def['icon'] === 'bell') + + @elseif($def['icon'] === 'sun') + + @elseif($def['icon'] === 'calendar-days') + + @elseif($def['icon'] === 'clipboard-document-check') + + @elseif($def['icon'] === 'cake') + + @elseif($def['icon'] === 'clock') + + @elseif($def['icon'] === 'moon') + + @elseif($def['icon'] === 'user-group') + + @elseif($def['icon'] === 'calendar') + + @endif +
    +
    +

    {{ t("automations.type.{$activeType}.name") }}

    +

    {{ t('automations.configure') }}

    +
    +
    + +
    + + {{-- Panel Body --}} +
    + + {{-- KANAL BADGE --}} +
    + Kanal: + @foreach($def['channels'] ?? ['push'] as $ch) + + {{ $ch === 'push' ? 'Push' : 'E-Mail' }} + + @endforeach +
    + + {{-- TYP-SPEZIFISCHE FELDER --}} + + @if($activeType === 'event_reminder') +
    + +
    + @foreach([15 => t('automations.time.15min'), 30 => t('automations.time.30min'), 60 => t('automations.time.1h'), 120 => t('automations.time.2h'), 1440 => t('automations.time.1d')] as $min => $label) + + @endforeach +
    +
    + @endif + + @if($activeType === 'daily_agenda') +
    + + +
    + @endif + + @if($activeType === 'weekly_overview') +
    + + +
    +
    + + +
    + @endif + + @if($activeType === 'event_followup') +
    + +
    + @foreach([0 => t('automations.delay.immediate'), 30 => t('automations.delay.30min'), 60 => t('automations.delay.1h')] as $min => $label) + + @endforeach +
    +
    + @endif + + @if($activeType === 'birthday_reminder') +
    + +
    + @foreach([0 => t('automations.before.today'), 1 => t('automations.before.1d'), 3 => t('automations.before.3d'), 7 => t('automations.before.1w')] as $days => $label) + + @endforeach +
    +
    + @endif + + @if($activeType === 'no_activity_reminder') +
    + +
    + @foreach([1 => t('automations.period.1d'), 3 => t('automations.period.3d'), 7 => t('automations.period.7d')] as $days => $label) + + @endforeach +
    +
    + @endif + + @if($activeType === 'daily_summary') +
    + + +
    + @endif + + @if($activeType === 'contact_followup') +
    + +
    + @foreach([7 => t('automations.after.1w'), 14 => t('automations.after.2w'), 30 => t('automations.after.1m'), 90 => t('automations.after.3m')] as $days => $label) + + @endforeach +
    +
    + @endif + + @if($activeType === 'free_slots_report') +
    + + +
    +
    + + +
    +
    + +
    + @foreach([30 => t('automations.slot.30min'), 60 => t('automations.slot.1h'), 120 => t('automations.slot.2h')] as $min => $label) + + @endforeach +
    +
    + @endif + + {{-- Beschreibung --}} +
    +
    + +

    {{ t("automations.type.{$activeType}.description") }}

    +
    +
    + +
    + + {{-- Panel Footer --}} +
    + + +
    + @endif + +
    + +
    diff --git a/src/resources/views/livewire/calendar/day-canvas.blade.php b/src/resources/views/livewire/calendar/day-canvas.blade.php new file mode 100644 index 0000000..7f3ad84 --- /dev/null +++ b/src/resources/views/livewire/calendar/day-canvas.blade.php @@ -0,0 +1,184 @@ +@php + $tz = auth()->user()->timezone; + $day = \Carbon\Carbon::parse($currentDate, $tz); + $today = now($tz)->toDateString(); + $isToday = $day->toDateString() === $today; + $nowLocal = now($tz); + $dayStr = $day->toDateString(); + + // Events fΓΌr diesen Tag aufteilen + $allDayEventsForDay = $events->filter(fn($e) => + $e->is_all_day + && $e->starts_local->toDateString() <= $dayStr + && ($e->ends_local ? $e->ends_local->toDateString() : $e->starts_local->toDateString()) >= $dayStr + ); + + // MehrtΓ€gige Termine: alle Events die diesen Tag ΓΌberspannen + $timedDayEvents = $events->filter(function ($e) use ($dayStr) { + if ($e->is_all_day) return false; + $eStart = $e->starts_local->toDateString(); + $eEnd = $e->ends_local ? $e->ends_local->toDateString() : $eStart; + return $eStart <= $dayStr && $eEnd >= $dayStr; + })->values(); + + // Greedy-Tiefenzuweisung – identischer Algorithmus wie WeekCanvas + $sorted = $timedDayEvents->sortBy(fn($e) => $e->starts_local->hour * 60 + $e->starts_local->minute)->values(); + $assigned = []; // ['start', 'end', 'depth'] + $dayLayouts = []; + + foreach ($sorted as $event) { + $endLocalEvt = $event->ends_local ?? $event->starts_local->copy()->addHour(); + $startDec = $event->starts_local->hour + $event->starts_local->minute / 60; + $endDec = min($endLocalEvt->hour + $endLocalEvt->minute / 60, 24.0); + + $usedDepths = []; + foreach ($assigned as $a) { + if ($startDec < $a['end'] && $endDec > $a['start']) { + $usedDepths[] = $a['depth']; + } + } + + $depth = 0; + while (in_array($depth, $usedDepths)) { $depth++; } + + $assigned[] = ['start' => $startDec, 'end' => $endDec, 'depth' => $depth]; + $dayLayouts[$event->id] = ['depth' => $depth]; + } +@endphp + +{{-- ── GanztΓ€gige Events (Tag-Ansicht) ── --}} +@if($allDayEventsForDay->isNotEmpty()) +
    +
    + {{ t('calendar.all_day') }} +
    +
    + @foreach($allDayEventsForDay as $event) + @php [$bg, $border, $text] = $resolveColor($event->color); @endphp +
    + {{ $event->title }} +
    + @endforeach +
    +
    +@endif + +{{-- ── Zeitachse ── --}} +
    + + {{-- Zeit-Labels --}} +
    + @for($h = $hourStart; $h <= $hourEnd; $h++) +
    + @if($h < $hourEnd) + {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00 + @endif +
    + @endfor +
    + + {{-- Tagesspalte --}} +
    + + {{-- Stunden-Linien --}} + @for($h = $hourStart; $h < $hourEnd; $h++) +
    + @endfor + + {{-- 15-Minuten-Linien --}} + @for($h = $hourStart; $h < $hourEnd; $h++) + @for($q = 1; $q <= 3; $q++) +
    + @endfor + @endfor + + {{-- Jetzt-Linie --}} + @if($isToday) + @php $nowPx = ($nowLocal->hour + $nowLocal->minute / 60 - $hourStart) * $cellPx; @endphp +
    +
    +
    +
    + @endif + + {{-- Events --}} + @foreach($timedDayEvents as $event) + @php + $startLocal = $event->starts_local; + $endLocal = $event->ends_local ?? $startLocal->copy()->addHour(); + + // Jeder Tag zeigt denselben Zeitslot (Original-Uhrzeit) + $startDec = $startLocal->hour + $startLocal->minute / 60; + $endDec = min($endLocal->hour + $endLocal->minute / 60, 24); + + $topPx = ($startDec - $hourStart) * $cellPx; + $heightPx = max(($endDec - $startDec) * $cellPx - 2, $cellPx / 4); + + [$bg, $border, $text] = $resolveColor($event->color); + + $layout = $dayLayouts[$event->id] ?? ['depth' => 0]; + $depth = $layout['depth']; + $leftPx = $depth * 12 + 2; + $zBase = 10 + $depth; + + $endsHere = $endLocal->toDateString() === $dayStr; + @endphp + +
    + +
    + {{ $event->title }} +
    + + @if($heightPx >= 28) +
    + {{ $startLocal->format('H:i') }} + @if($event->ends_local) – {{ $endLocal->format('H:i') }} @endif +
    + @endif + + {{-- Resize-Handle nur am letzten Tag --}} + @if($endsHere) +
    +
    +
    + @endif +
    + @endforeach + +
    +
    diff --git a/src/resources/views/livewire/calendar/forms/event-form.blade.php b/src/resources/views/livewire/calendar/forms/event-form.blade.php new file mode 100644 index 0000000..d56708e --- /dev/null +++ b/src/resources/views/livewire/calendar/forms/event-form.blade.php @@ -0,0 +1,338 @@ +
    + + {{-- TITLE --}} +
    + + +
    + + {{-- DATE & TIME --}} +
    + + +
    + + {{-- DATE --}} +
    + {{ t('events.date') }} + +
    + + {{-- MEHRTΓ„GIG --}} +
    + {{ t('events.multiday') }} + +
    + + {{-- MULTI DAY EXCEPTIONS --}} + @if($is_multi_day) +
    + +
    + {{ t('events.custom_days') }} +
    + + @foreach($this->days as $day) + @php + $d = $day->format('Y-m-d'); + $has = isset($exceptions[$d]); + @endphp + +
    +
    + {{ $day->translatedFormat('D d.m') }} +
    + + + + @if($has) +
    + + +
    + @endif +
    + @endforeach + +
    + @endif + + {{-- MULTI DATE --}} + @if($is_multi_day) +
    +
    + {{ t('common.from') }} + +
    +
    + {{ t('common.to') }} + +
    +
    + @endif + + {{-- TIME --}} + @if(!$is_all_day) +
    + {{ t('events.time') }} +
    + + +
    +
    + @endif + + {{-- ALL DAY --}} +
    + {{ t('events.all_day') }} + +
    + +
    +
    + + {{-- COLOR --}} +
    + + +
    + @foreach(['#6366f1','#22c55e','#ef4444','#f59e0b','#0ea5e9'] as $c) + + @endforeach +
    +
    + + {{-- TEILNEHMER --}} +
    + + + {{-- AusgewΓ€hlte Teilnehmer als Chips --}} + @if(count($attendees) > 0) +
    + @foreach($attendees as $attendee) + + {{ $attendee['name'] }} + + + @endforeach +
    + @endif + + {{-- Suchfeld --}} +
    + + + {{-- Dropdown VorschlΓ€ge --}} + @if(count($attendeeSuggestions) > 0) +
    + @foreach($attendeeSuggestions as $suggestion) + + @endforeach +
    + @endif +
    +
    + + {{-- NOTES --}} +
    + + + +
    + + {{-- ERINNERUNGEN --}} +
    + @php $locale = auth()->user()->locale ?? 'de'; @endphp + + + {{-- Bestehende Erinnerungen --}} + @foreach($reminders as $i => $r) +
    + + + @if(($r['type'] ?? '') === 'before') + @if(($r['minutes'] ?? 0) >= 1440) {{ t('events.reminder_1day') }} + @elseif(($r['minutes'] ?? 0) >= 60) {{ ($r['minutes'] ?? 0) / 60 }}{{ t('events.reminder_h_before') }} + @else {{ $r['minutes'] ?? 30 }}{{ t('events.reminder_min_before') }} + @endif + @elseif(($r['type'] ?? '') === 'time_of_day') + {{ t('events.reminder_on_day_prefix') }}{{ $r['time'] ?? '' }}{{ t('events.reminder_clock_suffix') }} + @else + {{ t('events.reminder_prev_day_prefix') }}{{ $r['time'] ?? '' }}{{ t('events.reminder_clock_suffix') }} + @endif + + +
    + @endforeach + + {{-- HinzufΓΌgen --}} +
    + + +
    + +
    +

    {{ t('events.reminder_before_section') }}

    +
    + @foreach([ + ['label' => '10 Min', 'min' => 10], + ['label' => '30 Min', 'min' => 30], + ['label' => '1 Std', 'min' => 60], + ['label' => '2 Std', 'min' => 120], + ['label' => '1 Tag', 'min' => 1440], + ] as $o) + + @endforeach +
    +
    + +
    +

    {{ t('events.reminder_on_day_section') }}

    +
    + @foreach(['07:00', '08:00', '09:00', '12:00'] as $time) + + @endforeach +
    +
    + +
    +

    {{ t('events.reminder_day_before_section') }}

    +
    + @foreach(['17:00', '18:00', '20:00'] as $time) + + @endforeach +
    +
    + + {{-- Custom Zeit --}} +
    +

    + {{ t('events.reminder_custom_time') }} +

    +
    + + + +
    +
    + +
    +
    +
    + + {{-- WIEDERHOLUNG --}} +
    + + + + + @if($recurrence) +
    + {{ t('events.recurrence_end') }} + +
    + @endif +
    + +
    diff --git a/src/resources/views/livewire/calendar/index.blade.php b/src/resources/views/livewire/calendar/index.blade.php new file mode 100644 index 0000000..6461341 --- /dev/null +++ b/src/resources/views/livewire/calendar/index.blade.php @@ -0,0 +1,314 @@ +
    +
    + +
    + {{-- NAV --}} +
    + + + +
    +
    +
    + {{ $date->translatedFormat('F Y') }} +
    +
    +
    + + {{-- VIEW SWITCH --}} +
    + + + +
    + +
    + + {{-- VIEW --}} + @if($view === 'month') + @include('livewire.calendar.views.month') + @endif + + @if($view === 'week') + @include('livewire.calendar.views.week') + @endif + + @if($view === 'day') + @include('livewire.calendar.views.day') + @endif + + {{-- SIDEBAR --}} + @livewire('calendar.sidebar') + +
    + + + + diff --git a/src/resources/views/livewire/calendar/modals/day-events-modal.blade.php b/src/resources/views/livewire/calendar/modals/day-events-modal.blade.php new file mode 100644 index 0000000..7dd65d3 --- /dev/null +++ b/src/resources/views/livewire/calendar/modals/day-events-modal.blade.php @@ -0,0 +1,63 @@ +
    + + {{-- HEADER --}} +
    + +
    + +
    + +
    +

    + {{ t('calendar.events') }} +

    + +

    + {{ \Carbon\Carbon::parse($date)->translatedFormat('l, d. F Y') }} +

    +
    + +
    + + {{-- BODY --}} +
    + + @forelse($events as $event) +
    +
    + {{ $event->title }} +
    + +
    + {{ $event->starts_at + ->timezone(auth()->user()->timezone) + ->format('H:i') }} +
    +
    + @empty +
    + {{ t('calendar.no_events') }} +
    + @endforelse + +
    + + {{-- FOOTER --}} +
    + + + +
    + +
    diff --git a/src/resources/views/livewire/calendar/modals/event-modal.blade.php b/src/resources/views/livewire/calendar/modals/event-modal.blade.php new file mode 100644 index 0000000..eb03812 --- /dev/null +++ b/src/resources/views/livewire/calendar/modals/event-modal.blade.php @@ -0,0 +1,362 @@ +
    + + {{-- HEADER --}} +
    +
    + +
    + +
    +

    + {{ t('calendar.edit_event') }} +

    + +

    + {{ \Carbon\Carbon::parse($date)->translatedFormat('l, d. F Y') }} +

    +
    +
    + + + {{-- BODY --}} +
    + + {{-- TITLE --}} +
    + + + +
    + + + {{-- πŸ”₯ GLOBAL SETTINGS --}} +
    + +
    + {{ t('events.standard_all_days') }} +
    + + {{-- DATE RANGE --}} +
    + +
    + + +
    + +
    + + +
    + +
    + + {{-- TIME --}} + @if(!$is_all_day) +
    + +
    + + +
    + +
    + + +
    + +
    + @endif + +
    + + + {{-- πŸ”₯ EXCEPTION --}} + {{-- πŸ”₯ EXCEPTION NUR BEI MULTI DAY --}} + @if($start_date !== $end_date) + +
    + +
    + +
    +
    + {{ t('events.adjust_day_only') }} +
    + +
    + {{ t('events.overrides_defaults') }} +
    +
    + + +
    + + @if($use_exception) + +
    + {{ t('events.changes_this_day') }} +
    + + @if($use_exception && !$is_all_day) +
    + +
    + + +
    + +
    + + +
    + +
    + @endif + + @endif + +
    + + @endif + +
    + + + {{-- FOOTER --}} +
    + + + + + +
    + +
    + + +{{--
    --}} + +{{-- --}}{{-- HEADER --}} +{{--
    --}} + +{{--
    --}} +{{-- --}} +{{--
    --}} + +{{--
    --}} +{{--

    --}} +{{-- {{ $eventId ? 'Termin bearbeiten' : 'Neuer Termin' }}--}} +{{--

    --}} + +{{--

    --}} +{{-- {{ \Carbon\Carbon::parse($date)->translatedFormat('l, d. F Y') }}--}} +{{--

    --}} +{{--
    --}} + +{{--
    --}} + + +{{-- --}}{{-- BODY --}} +{{--
    --}} + +{{-- --}}{{-- TITLE --}} +{{--
    --}} +{{-- --}} + +{{-- --}} +{{--
    --}} + +{{-- --}}{{-- START --}} +{{--
    --}} + +{{--
    --}} +{{-- --}} +{{-- --}} +{{--
    --}} + +{{--
    --}} +{{-- --}} +{{-- --}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- END DATUM --}} +{{--
    --}} +{{-- --}} + +{{-- --}} +{{--
    --}} + +{{-- --}}{{-- END TIME nur wenn END DATE gesetzt --}} +{{-- @if($end_date)--}} +{{--
    --}} +{{-- --}} + +{{-- --}} +{{--
    --}} +{{-- @endif--}} + +{{--
    --}} + +{{-- --}}{{-- FOOTER --}} +{{--
    --}} + +{{-- --}} +{{-- Abbrechen--}} +{{-- --}} + +{{-- --}} +{{-- Speichern--}} +{{-- --}} + +{{--
    --}} + +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{----}}{{-- HEADER --}} +{{--
    --}} + +{{--
    --}} +{{-- --}} +{{--
    --}} + +{{--
    --}} +{{--

    --}} +{{-- {{ $eventId ? 'Termin bearbeiten' : 'Neuer Termin' }}--}} +{{--

    --}} + +{{--

    --}} +{{-- {{ \Carbon\Carbon::parse($date)->translatedFormat('l, d. F Y') }}--}} +{{--

    --}} +{{--
    --}} + +{{--
    --}} + + +{{-- --}}{{----}}{{-- BODY --}} +{{--
    --}} + +{{-- --}}{{----}}{{-- TITLE --}} +{{--
    --}} +{{-- --}} + +{{-- --}} +{{--
    --}} + + +{{-- --}}{{----}}{{-- TIME --}} +{{--
    --}} +{{-- --}} + +{{-- --}} +{{--
    --}} + + +{{-- --}}{{----}}{{-- πŸ”₯ NEU: END DATE --}} +{{--
    --}} +{{-- --}} + +{{-- --}} +{{--
    --}} + + +{{--
    --}} +{{-- --}} +{{-- --}} +{{--
    --}} + +{{-- @if($use_end_time)--}} +{{-- --}} +{{-- @endif--}} +{{--
    --}} + + +{{-- --}}{{----}}{{-- FOOTER --}} +{{--
    --}} + +{{-- --}} +{{-- Abbrechen--}} +{{-- --}} + +{{-- --}} +{{-- Speichern--}} +{{-- --}} + +{{--
    --}} + +{{--
    --}} diff --git a/src/resources/views/livewire/calendar/month-canvas.blade.php b/src/resources/views/livewire/calendar/month-canvas.blade.php new file mode 100644 index 0000000..4c20884 --- /dev/null +++ b/src/resources/views/livewire/calendar/month-canvas.blade.php @@ -0,0 +1,114 @@ +@php + $tz = auth()->user()->timezone; + $today = now($tz)->toDateString(); + $currentMonthNr = \Carbon\Carbon::parse($currentDate, $tz)->month; + + $start = \Carbon\Carbon::parse($currentDate, $tz)->startOfMonth()->startOfWeek(\Carbon\Carbon::MONDAY); + $end = \Carbon\Carbon::parse($currentDate, $tz)->endOfMonth()->endOfWeek(\Carbon\Carbon::SUNDAY); + + $days = collect(); + $cursor = $start->copy(); + while ($cursor <= $end) { + $days->push($cursor->copy()); + $cursor->addDay(); + } +@endphp + +{{-- Wochentag-Header --}} +
    + @foreach([t('calendar.days.mo'), t('calendar.days.di'), t('calendar.days.mi'), t('calendar.days.do'), t('calendar.days.fr'), t('calendar.days.sa'), t('calendar.days.so')] as $dn) +
    {{ $dn }}
    + @endforeach +
    + +{{-- Tage-Grid --}} +
    + @foreach($days as $day) + @php + $dayStr = $day->toDateString(); + $isToday = $dayStr === $today; + $isCurrentMonth = $day->month === $currentMonthNr; + + // GanztΓ€gige Events, die diesen Tag abdecken + $allDayForDay = $events->filter(fn($e) => + $e->is_all_day + && $e->starts_local->toDateString() <= $dayStr + && ($e->ends_local ? $e->ends_local->toDateString() : $e->starts_local->toDateString()) >= $dayStr + ); + + // Zeitgebundene Events, die diesen Tag ΓΌberspannen (mehrtΓ€gig: jeden Tag anzeigen) + $timedForDay = $events->filter(function ($e) use ($dayStr) { + if ($e->is_all_day) return false; + $eStart = $e->starts_local->toDateString(); + $eEnd = $e->ends_local ? $e->ends_local->toDateString() : $eStart; + return $eStart <= $dayStr && $eEnd >= $dayStr; + }); + + $totalVisible = $allDayForDay->count() + $timedForDay->count(); + $maxShow = 3; + $shown = 0; + @endphp + +
    + + {{-- Tag-Nummer --}} +
    + + {{ $day->day }} + +
    + + {{-- GanztΓ€gige Events --}} + @foreach($allDayForDay->take($maxShow) as $event) + @php + [$bg, $border, $text] = $resolveColor($event->color); + $shown++; + @endphp +
    + {{ $event->title }} +
    + @endforeach + + {{-- Zeitgebundene Events --}} + @foreach($timedForDay as $event) + @if($shown >= $maxShow) @break @endif + @php + [$bg, $border, $text] = $resolveColor($event->color); + $shown++; + @endphp +
    + {{ $event->starts_local->format('H:i') }} + {{ $event->title }} +
    + @endforeach + + {{-- Overflow --}} + @if($totalVisible > $maxShow) + + +{{ $totalVisible - $maxShow }} {{ t('calendar.more') }} + + @endif + +
    + @endforeach +
    diff --git a/src/resources/views/livewire/calendar/sidebar.blade.php b/src/resources/views/livewire/calendar/sidebar.blade.php new file mode 100644 index 0000000..4dbae37 --- /dev/null +++ b/src/resources/views/livewire/calendar/sidebar.blade.php @@ -0,0 +1,93 @@ +
    + + {{-- BACKDROP --}} +
    + + {{-- SIDEBAR --}} +
    + + {{-- HEADER --}} +
    + +
    +
    + +
    + +
    +
    + {{ $mode === 'edit' ? t('calendar.edit_event') : t('calendar.new_event') }} +
    + @if($date) +
    + {{ \Carbon\Carbon::parse($date)->translatedFormat('l, d. F Y') }} +
    + @endif +
    +
    + + + +
    + + + {{-- CONTENT --}} +
    + + @livewire('calendar.forms.event-form', [ + 'eventId' => $eventId, + 'starts_at' => $date, + 'time' => $time, + ], key(($eventId ?? 'create') . '-' . $date . '-' . $time)) + +
    + + + {{-- FOOTER --}} +
    + + @if($eventId) + + @else + + @endif + + + +
    + +
    + +
    diff --git a/src/resources/views/livewire/calendar/skeletons/day.blade.php b/src/resources/views/livewire/calendar/skeletons/day.blade.php new file mode 100644 index 0000000..af26f6d --- /dev/null +++ b/src/resources/views/livewire/calendar/skeletons/day.blade.php @@ -0,0 +1,34 @@ +{{-- Day Skeleton – gleiche Struktur wie echte Tagesansicht --}} +@php $cellPx = 48; $totalPx = 24 * $cellPx; @endphp +
    + + {{-- Scroll-Bereich --}} +
    +
    + + {{-- Zeit-Labels (w-12 wie echt) --}} +
    + @for($h = 0; $h < 24; $h++) +
    +
    +
    + @endfor +
    + + {{-- Tagesspalte (flex-1 wie echt) --}} +
    + + {{-- Stunden-Linien --}} + @for($h = 0; $h < 24; $h++) +
    + @endfor + + {{-- Fake Events --}} +
    +
    +
    + +
    +
    +
    diff --git a/src/resources/views/livewire/calendar/skeletons/month.blade.php b/src/resources/views/livewire/calendar/skeletons/month.blade.php new file mode 100644 index 0000000..ad821a6 --- /dev/null +++ b/src/resources/views/livewire/calendar/skeletons/month.blade.php @@ -0,0 +1,43 @@ +{{-- Month Skeleton – gleiche Struktur wie echte Monatsansicht --}} +
    + + {{-- Scroll-Bereich (wie echt: overflow-y-auto + max-height) --}} +
    + + {{-- Wochentag-Header (grid grid-cols-7 wie echt) --}} +
    + @foreach(['Mo','Di','Mi','Do','Fr','Sa','So'] as $dn) +
    +
    +
    + @endforeach +
    + + {{-- Tage-Grid (grid grid-cols-7 divide wie echt) --}} +
    + @for($cell = 0; $cell < 35; $cell++) + @php $row = intdiv($cell, 7); $col = $cell % 7; @endphp +
    + + {{-- Tag-Nummer-Platzhalter --}} +
    +
    +
    + + {{-- Fake Event-Balken (verteilt) --}} + @if(($row === 0 && $col === 1) || ($row === 1 && $col === 3) || ($row === 2 && $col === 0) || ($row === 3 && $col === 4) || ($row === 4 && $col === 2)) +
    + @endif + @if(($row === 0 && $col === 4) || ($row === 1 && $col === 0) || ($row === 2 && $col === 5) || ($row === 3 && $col === 2) || ($row === 4 && $col === 6)) +
    + @endif + @if(($row === 0 && $col === 3) || ($row === 2 && $col === 3) || ($row === 3 && $col === 6)) +
    +
    + @endif +
    + @endfor +
    + +
    +
    diff --git a/src/resources/views/livewire/calendar/skeletons/week.blade.php b/src/resources/views/livewire/calendar/skeletons/week.blade.php new file mode 100644 index 0000000..94dde5c --- /dev/null +++ b/src/resources/views/livewire/calendar/skeletons/week.blade.php @@ -0,0 +1,64 @@ +{{-- Week Skeleton – gleiche Struktur wie echte Wochenansicht --}} +@php $cellPx = 48; $totalPx = 24 * $cellPx; @endphp +
    + + {{-- Tagesspalten-Header (identisch zu week-canvas: flex mit w-12 spacer + 7 flex-1) --}} +
    +
    + @for($i = 0; $i < 7; $i++) +
    +
    +
    +
    + @endfor +
    + + {{-- Scroll-Bereich --}} +
    +
    + + {{-- Zeit-Labels (w-12 wie echt) --}} +
    + @for($h = 0; $h < 24; $h++) +
    +
    +
    + @endfor +
    + + {{-- 7 Tagesspalten (flex-1 wie echt) --}} + @for($col = 0; $col < 7; $col++) +
    + + {{-- Stunden-Linien --}} + @for($h = 0; $h < 24; $h++) +
    + @endfor + + {{-- Fake Events (verteilt ΓΌber verschiedene Spalten & Zeiten) --}} + @if($col === 0) +
    + @endif + @if($col === 1) +
    + @endif + @if($col === 2) +
    + @endif + @if($col === 3) +
    +
    + @endif + @if($col === 5) +
    + @endif + @if($col === 6) +
    + @endif +
    + @endfor + +
    +
    +
    diff --git a/src/resources/views/livewire/calendar/views/day-canvas.blade.php b/src/resources/views/livewire/calendar/views/day-canvas.blade.php new file mode 100644 index 0000000..77de9cf --- /dev/null +++ b/src/resources/views/livewire/calendar/views/day-canvas.blade.php @@ -0,0 +1,184 @@ +@php + $tz = auth()->user()->timezone; + $day = \Carbon\Carbon::parse($currentDate, $tz); + $today = now($tz)->toDateString(); + $isToday = $day->toDateString() === $today; + $nowLocal = now($tz); + $dayStr = $day->toDateString(); + + // Events fΓΌr diesen Tag aufteilen + $allDayEventsForDay = $events->filter(fn($e) => + $e->is_all_day + && $e->starts_local->toDateString() <= $dayStr + && ($e->ends_local ? $e->ends_local->toDateString() : $e->starts_local->toDateString()) >= $dayStr + ); + + // MehrtΓ€gige Termine: alle Events die diesen Tag ΓΌberspannen + $timedDayEvents = $events->filter(function ($e) use ($dayStr) { + if ($e->is_all_day) return false; + $eStart = $e->starts_local->toDateString(); + $eEnd = $e->ends_local ? $e->ends_local->toDateString() : $eStart; + return $eStart <= $dayStr && $eEnd >= $dayStr; + })->values(); + + // Greedy-Tiefenzuweisung – identischer Algorithmus wie WeekCanvas + $sorted = $timedDayEvents->sortBy(fn($e) => $e->starts_local->hour * 60 + $e->starts_local->minute)->values(); + $assigned = []; // ['start', 'end', 'depth'] + $dayLayouts = []; + + foreach ($sorted as $event) { + $endLocalEvt = $event->ends_local ?? $event->starts_local->copy()->addHour(); + $startDec = $event->starts_local->hour + $event->starts_local->minute / 60; + $endDec = min($endLocalEvt->hour + $endLocalEvt->minute / 60, 24.0); + + $usedDepths = []; + foreach ($assigned as $a) { + if ($startDec < $a['end'] && $endDec > $a['start']) { + $usedDepths[] = $a['depth']; + } + } + + $depth = 0; + while (in_array($depth, $usedDepths)) { $depth++; } + + $assigned[] = ['start' => $startDec, 'end' => $endDec, 'depth' => $depth]; + $dayLayouts[$event->id] = ['depth' => $depth]; + } +@endphp + +{{-- ── GanztΓ€gige Events (Tag-Ansicht) ── --}} +@if($allDayEventsForDay->isNotEmpty()) +
    +
    + ganzt. +
    +
    + @foreach($allDayEventsForDay as $event) + @php [$bg, $border, $text] = $resolveColor($event->color); @endphp +
    + {{ $event->title }} +
    + @endforeach +
    +
    +@endif + +{{-- ── Zeitachse ── --}} +
    + + {{-- Zeit-Labels --}} +
    + @for($h = $hourStart; $h <= $hourEnd; $h++) +
    + @if($h < $hourEnd) + {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00 + @endif +
    + @endfor +
    + + {{-- Tagesspalte --}} +
    + + {{-- Stunden-Linien --}} + @for($h = $hourStart; $h < $hourEnd; $h++) +
    + @endfor + + {{-- 15-Minuten-Linien --}} + @for($h = $hourStart; $h < $hourEnd; $h++) + @for($q = 1; $q <= 3; $q++) +
    + @endfor + @endfor + + {{-- Jetzt-Linie --}} + @if($isToday) + @php $nowPx = ($nowLocal->hour + $nowLocal->minute / 60 - $hourStart) * $cellPx; @endphp +
    +
    +
    +
    + @endif + + {{-- Events --}} + @foreach($timedDayEvents as $event) + @php + $startLocal = $event->starts_local; + $endLocal = $event->ends_local ?? $startLocal->copy()->addHour(); + + // Jeder Tag zeigt denselben Zeitslot (Original-Uhrzeit) + $startDec = $startLocal->hour + $startLocal->minute / 60; + $endDec = min($endLocal->hour + $endLocal->minute / 60, 24); + + $topPx = ($startDec - $hourStart) * $cellPx; + $heightPx = max(($endDec - $startDec) * $cellPx - 2, $cellPx / 4); + + [$bg, $border, $text] = $resolveColor($event->color); + + $layout = $dayLayouts[$event->id] ?? ['depth' => 0]; + $depth = $layout['depth']; + $leftPx = $depth * 12 + 2; + $zBase = 10 + $depth; + + $endsHere = $endLocal->toDateString() === $dayStr; + @endphp + +
    + +
    + {{ $event->title }} +
    + + @if($heightPx >= 28) +
    + {{ $startLocal->format('H:i') }} + @if($event->ends_local) – {{ $endLocal->format('H:i') }} @endif +
    + @endif + + {{-- Resize-Handle nur am letzten Tag --}} + @if($endsHere) +
    +
    +
    + @endif +
    + @endforeach + +
    +
    diff --git a/src/resources/views/livewire/calendar/views/month-canvas.blade.php b/src/resources/views/livewire/calendar/views/month-canvas.blade.php new file mode 100644 index 0000000..271bdfd --- /dev/null +++ b/src/resources/views/livewire/calendar/views/month-canvas.blade.php @@ -0,0 +1,114 @@ +@php + $tz = auth()->user()->timezone; + $today = now($tz)->toDateString(); + $currentMonthNr = \Carbon\Carbon::parse($currentDate, $tz)->month; + + $start = \Carbon\Carbon::parse($currentDate, $tz)->startOfMonth()->startOfWeek(\Carbon\Carbon::MONDAY); + $end = \Carbon\Carbon::parse($currentDate, $tz)->endOfMonth()->endOfWeek(\Carbon\Carbon::SUNDAY); + + $days = collect(); + $cursor = $start->copy(); + while ($cursor <= $end) { + $days->push($cursor->copy()); + $cursor->addDay(); + } +@endphp + +{{-- Wochentag-Header --}} +
    + @foreach(['Mo','Di','Mi','Do','Fr','Sa','So'] as $dn) +
    {{ $dn }}
    + @endforeach +
    + +{{-- Tage-Grid --}} +
    + @foreach($days as $day) + @php + $dayStr = $day->toDateString(); + $isToday = $dayStr === $today; + $isCurrentMonth = $day->month === $currentMonthNr; + + // GanztΓ€gige Events, die diesen Tag abdecken + $allDayForDay = $events->filter(fn($e) => + $e->is_all_day + && $e->starts_local->toDateString() <= $dayStr + && ($e->ends_local ? $e->ends_local->toDateString() : $e->starts_local->toDateString()) >= $dayStr + ); + + // Zeitgebundene Events, die diesen Tag ΓΌberspannen (mehrtΓ€gig: jeden Tag anzeigen) + $timedForDay = $events->filter(function ($e) use ($dayStr) { + if ($e->is_all_day) return false; + $eStart = $e->starts_local->toDateString(); + $eEnd = $e->ends_local ? $e->ends_local->toDateString() : $eStart; + return $eStart <= $dayStr && $eEnd >= $dayStr; + }); + + $totalVisible = $allDayForDay->count() + $timedForDay->count(); + $maxShow = 3; + $shown = 0; + @endphp + +
    + + {{-- Tag-Nummer --}} +
    + + {{ $day->day }} + +
    + + {{-- GanztΓ€gige Events --}} + @foreach($allDayForDay->take($maxShow) as $event) + @php + [$bg, $border, $text] = $resolveColor($event->color); + $shown++; + @endphp +
    + {{ $event->title }} +
    + @endforeach + + {{-- Zeitgebundene Events --}} + @foreach($timedForDay as $event) + @if($shown >= $maxShow) @break @endif + @php + [$bg, $border, $text] = $resolveColor($event->color); + $shown++; + @endphp +
    + {{ $event->starts_local->format('H:i') }} + {{ $event->title }} +
    + @endforeach + + {{-- Overflow --}} + @if($totalVisible > $maxShow) + + +{{ $totalVisible - $maxShow }} mehr + + @endif + +
    + @endforeach +
    diff --git a/src/resources/views/livewire/calendar/views/week-canvas.blade.php b/src/resources/views/livewire/calendar/views/week-canvas.blade.php new file mode 100644 index 0000000..006cb14 --- /dev/null +++ b/src/resources/views/livewire/calendar/views/week-canvas.blade.php @@ -0,0 +1,129 @@ +
    + + {{-- Zeit-Labels --}} +
    + @for($h = $hourStart; $h <= $hourEnd; $h++) +
    + @if($h < $hourEnd) + {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00 + @endif +
    + @endfor +
    + + {{-- ── 7 Tagesspalten ── --}} + @foreach($weekDays as $colIndex => $day) + @php + $dayStr = $day->toDateString(); + $isToday = $dayStr === $today; + $dayEvents = $eventsByDay[$colIndex] ?? collect(); + @endphp + +
    + + {{-- Stunden-Linien --}} + @for($h = $hourStart; $h < $hourEnd; $h++) +
    + @endfor + + {{-- 15-Minuten-Linien (sehr subtil) --}} + @for($h = $hourStart; $h < $hourEnd; $h++) + @for($q = 1; $q <= 3; $q++) +
    + @endfor + @endfor + + {{-- Jetzt-Linie --}} + @if($isToday) + @php $nowPx = ($nowLocal->hour + $nowLocal->minute / 60 - $hourStart) * $cellPx; @endphp +
    +
    +
    +
    + @endif + + {{-- Events (timed; ganztΓ€gige werden oben im All-Day-Stripe gezeigt) --}} + @foreach($dayEvents as $event) + @php + $startLocal = $event->starts_local; + $endLocal = $event->ends_local ?? $startLocal->copy()->addHour(); + + // Jeder Tag zeigt denselben Zeitslot (Original-Uhrzeit) + $startDec = $startLocal->hour + $startLocal->minute / 60; + $endDec = min($endLocal->hour + $endLocal->minute / 60, 24); + + $topPx = ($startDec - $hourStart) * $cellPx; + $heightPx = max(($endDec - $startDec) * $cellPx - 2, $cellPx / 4); + + [$bg, $border, $text] = $resolveColor($event->color); + + // Stacking: depth=0 β†’ volle Breite, depth>0 β†’ links eingerΓΌckt + $layout = $layoutsByDay[$colIndex][$event->id] ?? ['depth' => 0]; + $depth = $layout['depth']; + $leftPx = $depth * 12 + 2; // 12 px Offset pro Ebene + $zBase = 10 + $depth; + + $endsHere = $endLocal->toDateString() === $dayStr; + @endphp + +
    + +
    +
    + {{ $event->title }} +
    + @if($event->recurrence) + + + + @endif +
    + + @if($heightPx >= 28) +
    + {{ $startLocal->format('H:i') }} + @if($event->ends_local) – {{ $endLocal->format('H:i') }} @endif +
    + @endif + + {{-- Resize-Handle nur am letzten Tag --}} + @if($endsHere) +
    +
    +
    + @endif +
    + + @endforeach +
    + @endforeach + +
    diff --git a/src/resources/views/livewire/calendar/views_alt/day.blade.php b/src/resources/views/livewire/calendar/views_alt/day.blade.php new file mode 100644 index 0000000..0a27980 --- /dev/null +++ b/src/resources/views/livewire/calendar/views_alt/day.blade.php @@ -0,0 +1,101 @@ +@php + $dayData = $this->calendarDays->firstWhere('key', $date->format('Y-m-d')) ?? []; + + $day = $dayData['date'] ?? $date; + $key = $dayData['key'] ?? $date->format('Y-m-d'); + + $allDayEvents = collect($dayData['allDay'] ?? []); + $timedEvents = collect($dayData['timed'] ?? []); + + $hourHeight = 64; +@endphp + +
    + + {{-- HEADER --}} +
    +

    + {{ $day->translatedFormat('l, d. F Y') }} +

    +
    + + {{-- ALL DAY --}} + @if($allDayEvents->isNotEmpty()) +
    + + @foreach($allDayEvents as $event) +
    + {{ $event->title }} +
    + @endforeach + +
    + @endif + + {{-- TIMELINE --}} +
    + + {{-- πŸ”₯ ZEITSPALTE --}} +
    + + @for($h = 0; $h < 24; $h++) +
    + {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00 +
    + @endfor + +
    + + {{-- πŸ”₯ GRID + EVENTS --}} +
    + + {{-- GRID --}} + @for($h = 0; $h < 24; $h++) +
    + @endfor + + {{-- EVENTS --}} + @foreach($timedEvents as $event) + + @php + $gap = 6; + $width = 100 / $event['count']; + @endphp + +
    +
    + {{ $event['start']->format('H:i') }} + @if($event['end']) + – {{ $event['end']->format('H:i') }} + @endif +
    + +
    + {{ $event['title'] }} +
    + +
    + + @endforeach + +
    + +
    +
    diff --git a/src/resources/views/livewire/calendar/views_alt/month.blade.php b/src/resources/views/livewire/calendar/views_alt/month.blade.php new file mode 100644 index 0000000..8b1663e --- /dev/null +++ b/src/resources/views/livewire/calendar/views_alt/month.blade.php @@ -0,0 +1,643 @@ +
    + @foreach(['Mo','Di','Mi','Do','Fr','Sa','So'] as $day) +
    + {{ $day }} +
    + @endforeach +
    + +
    + + @php + $tz = auth()->user()->timezone; + + $currentMonth = $date->copy()->startOfMonth(); + $start = $date->copy()->startOfMonth()->startOfWeek(); + $end = $date->copy()->endOfMonth()->endOfWeek(); + + $period = \Carbon\CarbonPeriod::create($start, $end); + @endphp + + @foreach ($period as $day) + + @php + $key = $day->format('Y-m-d'); + $dayEvents = $events[$key] ?? collect(); + + $isToday = $day->isSameDay(now($tz)); + $isCurrentMonth = $day->month === $currentMonth->month; + $isWeekend = $day->isWeekend(); + @endphp + + {{-- πŸ”₯ DAY CARD (DROP ZONE) --}} +{{-- + + {{-- HEADER --}} +
    + + {{ $day->format('d') }} + + + @if($isToday) + + Heute + + @endif +
    + + {{-- EVENTS --}} + @php + $count = $dayEvents->count(); + + if ($count <= 2) { + $visibleEvents = $dayEvents; + $remainingCount = 0; + } else { + $visibleEvents = $dayEvents->take(1); + $remainingCount = $count - 1; + } + @endphp + + @foreach($visibleEvents as $event) + + @php + $time = $event->getDisplayTimeForDate($key, $tz); + $start = $event->starts_at->copy()->setTimezone($tz)->startOfDay(); + $current = \Carbon\Carbon::parse($key, $tz)->startOfDay(); + + $index = $start->diffInDays($current); + + @endphp + + {{-- πŸ”₯ DRAGGABLE EVENT --}} +
    + + @if(!$event->is_all_day) + @if($time['end']) + {{ $time['start']->format('H:i') }} - {{ $time['end']->format('H:i') }} + @else + {{ $time['start']->format('H:i') }} + @endif + @endif + + {{ $event->title }} +
    + + + @endforeach + + @if($remainingCount > 0) +
    + +{{ $remainingCount }} mehr +
    + @endif + + +
    + @endforeach + + + + +{{-- πŸ”₯ DRAG SCRIPT --}} + +{{----}} + + + + +{{----}} +{{----}} + +{{--
    --}} +{{-- @foreach(['Mo','Di','Mi','Do','Fr','Sa','So'] as $day)--}} +{{--
    --}} +{{-- {{ $day }}--}} +{{--
    --}} +{{-- @endforeach--}} +{{--
    --}} + +{{--
    --}} + +{{-- @php--}} +{{-- $tz = auth()->user()->timezone;--}} + +{{-- $currentMonth = $date->copy()->startOfMonth();--}} + +{{-- $start = $date->copy()->startOfMonth()->startOfWeek();--}} +{{-- $end = $date->copy()->endOfMonth()->endOfWeek();--}} +{{-- @endphp--}} + +{{-- @php--}} +{{-- $period = \Carbon\CarbonPeriod::create($start, $end);--}} +{{-- @endphp--}} + +{{-- @foreach ($period as $day)--}} +{{-- @for ($day = $start->copy(); $day <= $end; $day->addDay())--}} + +{{-- @php--}} +{{-- $key = $day->format('Y-m-d');--}} +{{-- $dayEvents = $events[$key] ?? collect();--}} + +{{-- $isToday = $day->isSameDay(now($tz));--}} +{{-- $isCurrentMonth = $day->month === $currentMonth->month;--}} +{{-- $isWeekend = $day->isWeekend();--}} +{{-- @endphp--}} + +{{-- --}} +{{-- --}}{{-- HEADER --}} +{{--
    --}} + +{{-- --}} +{{-- {{ $day->format('d') }}--}} +{{-- --}} + +{{-- @if($isToday)--}} +{{-- --}} +{{-- Heute--}} +{{-- --}} +{{-- @endif--}} +{{--
    --}} + +{{-- --}}{{-- EVENTS --}} +{{-- @php--}} +{{-- $count = $dayEvents->count();--}} + +{{-- if ($count <= 2) {--}} +{{-- $visibleEvents = $dayEvents;--}} +{{-- $remainingCount = 0;--}} +{{-- } else {--}} +{{-- $visibleEvents = $dayEvents->take(1);--}} +{{-- $remainingCount = $count - 1;--}} +{{-- }--}} +{{-- @endphp--}} + +{{-- @foreach($visibleEvents as $event)--}} + +{{-- @php--}} +{{-- $time = $event->getDisplayTimeForDate($key, $tz);--}} +{{-- @endphp--}} + +{{-- --}} + +{{-- @if(!$event->is_all_day)--}} + +{{-- @if($time['end'])--}} +{{-- {{ $time['start']->format('H:i') }} - {{ $time['end']->format('H:i') }}--}} +{{-- @else--}} +{{-- {{ $time['start']->format('H:i') }}--}} +{{-- @endif--}} + +{{-- @endif--}} + +{{-- {{ $event->title }}--}} + +{{--
    --}} + +{{-- @endforeach--}} + +{{-- @if($remainingCount > 0)--}} +{{--
    --}} +{{-- +{{ $remainingCount }} mehr--}} +{{--
    --}} +{{-- @endif--}} + +{{-- --}} + +{{-- @endfor--}} +{{-- @endforeach--}} +{{----}} diff --git a/src/resources/views/livewire/calendar/views_alt/week.blade.php b/src/resources/views/livewire/calendar/views_alt/week.blade.php new file mode 100644 index 0000000..65cda26 --- /dev/null +++ b/src/resources/views/livewire/calendar/views_alt/week.blade.php @@ -0,0 +1,1130 @@ +@php + $tz = auth()->user()->timezone; + $now = now($tz); + + $hourHeight = 64; + $nowTop = ($now->hour * $hourHeight) + (($now->minute / 60) * $hourHeight); +@endphp + +
    + + {{-- HEADER + ALLDAY --}} +
    + + {{-- HEADER --}} +
    + +
    + + @foreach($this->calendarDays as $day) +
    +
    + {{ $day['date']->translatedFormat('D') }} +
    +
    + {{ $day['date']->format('d') }} +
    +
    + @endforeach + +
    + + {{-- πŸ”₯ ALL DAY ROW --}} +
    + + + {{-- TIME COLUMN --}} +
    + + + {{-- EVENTS CONTAINER --}} +
    + + @foreach($this->allDayEvents as $event) + +
    + {{ $event['title'] }} +
    + + @endforeach + +
    + +
    +
    + + {{-- MAIN --}} +
    + +
    + + {{-- TIME COLUMN --}} +
    + + @for($h = 0; $h < 24; $h++) +
    + {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00 +
    + @endfor + + {{-- NOW LABEL --}} +
    + {{ $now->format('H:i') }} +
    + +
    + + {{-- DAYS --}} + @foreach($this->calendarDays as $day) + +
    + +
    + + @for($h = 0; $h < 24; $h++) +
    + @endfor + + + + {{-- EVENTS --}} + @foreach($day['timed'] as $event) + +
    + +
    + +
    +
    + {{ $event['title'] }} +
    + +
    + {{ $event['start']->format('H:i') }} + @if($event['end']) + – {{ $event['end']->format('H:i') }} + @endif +
    +
    + +
    + +
    + + @endforeach + +
    +
    + + @endforeach + +
    + + {{-- NOW LINE --}} +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    + +
    + +
    + + +{{--
    --}} + +{{-- --}}{{-- HEADER + ALLDAY --}} +{{--
    --}} + +{{-- --}}{{-- HEADER --}} +{{--
    --}} + +{{--
    --}} + +{{-- @foreach($this->calendarDays as $day)--}} +{{--
    --}} +{{--
    --}} +{{-- {{ $day['date']->translatedFormat('D') }}--}} +{{--
    --}} +{{--
    --}} +{{-- {{ $day['date']->format('d') }}--}} +{{--
    --}} +{{--
    --}} +{{-- @endforeach--}} + +{{--
    --}} + +{{-- --}}{{-- πŸ”₯ ALL DAY ROW --}} +{{-- --}} + +{{--
    --}} + +{{--
    --}} + +{{-- @foreach($this->allDayEvents as $event)--}} + +{{-- --}} +{{-- {{ $event['title'] }}--}} +{{--
    --}} + +{{-- @endforeach--}} + +{{-- --}}{{-- DROP ZONES --}} +{{-- @foreach(range(0,6) as $i)--}} +{{--
    --}} +{{-- @endforeach--}} + +{{--
    --}} + +{{-- --}} +{{-- --}} + +{{-- --}}{{-- MAIN --}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- TIME COLUMN --}} +{{--
    --}} + +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}}{{-- NOW LABEL --}} +{{--
    --}} +{{-- {{ $now->format('H:i') }}--}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- DAYS --}} +{{-- @foreach($this->calendarDays as $day)--}} + +{{-- --}} + +{{--
    --}} + +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}} + +{{-- --}}{{-- EVENTS --}} +{{-- @foreach($day['timed'] as $event)--}} + +{{-- --}} + +{{-- --}}{{-- RESIZE TOP --}} +{{--
    --}} + +{{-- --}}{{-- CONTENT --}} +{{--
    --}} +{{--
    --}} +{{-- {{ $event['title'] }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $event['start']->format('H:i') }}--}} +{{-- @if($event['end'])--}} +{{-- – {{ $event['end']->format('H:i') }}--}} +{{-- @endif--}} +{{--
    --}} +{{--
    --}} + +{{-- --}}{{-- RESIZE BOTTOM --}} +{{--
    --}} + +{{--
    --}} + +{{-- @endforeach--}} + +{{--
    --}} +{{--
    --}} + +{{-- @endforeach--}} + +{{-- --}} + +{{-- --}}{{-- NOW LINE --}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} +{{--
    --}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} + +{{-- --}} + +{{----}} + +{{--@php--}} +{{-- $tz = auth()->user()->timezone;--}} +{{-- $now = now($tz);--}} + +{{-- $hourHeight = 64;--}} +{{-- $nowTop = ($now->hour * $hourHeight) + (($now->minute / 60) * $hourHeight);--}} +{{--@endphp--}} + +{{--
    --}} + +{{-- --}}{{-- HEADER + ALLDAY --}} +{{--
    --}} + +{{-- --}}{{-- HEADER --}} +{{--
    --}} + +{{--
    --}} + +{{-- @foreach($this->calendarDays as $day)--}} +{{--
    --}} +{{--
    --}} +{{-- {{ $day['date']->translatedFormat('D') }}--}} +{{--
    --}} +{{--
    --}} +{{-- {{ $day['date']->format('d') }}--}} +{{--
    --}} +{{--
    --}} +{{-- @endforeach--}} + +{{--
    --}} + +{{-- --}}{{-- πŸ”₯ ALL DAY ROW --}} +{{-- --}} + +{{-- @php--}} +{{-- $weekStart = $this->date->copy()->startOfWeek();--}} +{{-- @endphp--}} + +{{-- @foreach($this->allDayEvents as $event)--}} + +{{-- @php--}} +{{-- $startOffset = $event['start']->diffInDays($weekStart, false);--}} +{{-- $endOffset = $event['end']->diffInDays($weekStart, false);--}} + +{{-- $startOffset = max($startOffset, 0);--}} +{{-- $endOffset = min($endOffset, 6);--}} + +{{-- $span = ($endOffset - $startOffset) + 1;--}} +{{-- @endphp--}} + +{{-- --}} +{{-- {{ $event['title'] }}--}} +{{--
    --}} + +{{-- @endforeach--}} + +{{--
    --}} + +{{-- --}} + +{{-- --}}{{-- πŸ”₯ MAIN GRID --}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- TIME COLUMN --}} +{{--
    --}} + +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}}{{-- NOW LABEL --}} +{{--
    --}} +{{-- {{ $now->format('H:i') }}--}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- DAYS --}} +{{-- @foreach($this->calendarDays as $day)--}} + +{{-- --}} + +{{-- --}}{{-- GRID --}} +{{--
    --}} + +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}}{{-- EVENTS --}} +{{-- @foreach($day['timed'] as $event)--}} + +{{-- --}} + +{{--
    --}} +{{--
    --}} +{{-- {{ $event['title'] }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $event['start']->format('H:i') }}--}} +{{-- @if($event['end'])--}} +{{-- – {{ $event['end']->format('H:i') }}--}} +{{-- @endif--}} +{{--
    --}} +{{--
    --}} + +{{--
    --}} + +{{-- @endforeach--}} + +{{--
    --}} +{{--
    --}} + +{{-- @endforeach--}} + +{{-- --}} + +{{-- --}}{{-- NOW LINE --}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} +{{--
    --}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} + +{{-- --}} +{{----}} + +{{--
    --}} + +{{-- --}}{{-- HEADER + ALLDAY --}} +{{--
    --}} + +{{-- --}}{{-- HEADER --}} +{{--
    --}} + +{{--
    --}} + +{{-- @foreach($this->calendarDays as $day)--}} +{{--
    --}} +{{--
    --}} +{{-- {{ $day['date']->translatedFormat('D') }}--}} +{{--
    --}} +{{--
    --}} +{{-- {{ $day['date']->format('d') }}--}} +{{--
    --}} +{{--
    --}} +{{-- @endforeach--}} + +{{--
    --}} + +{{-- --}}{{-- πŸ”₯ ALL DAY ROW --}} +{{-- --}} + +{{-- @foreach($this->allDayEvents as $event)--}} + +{{-- --}} +{{-- {{ $event['title'] }}--}} +{{--
    --}} + +{{-- @endforeach--}} + +{{--
    --}} + +{{-- --}} + +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- TIME COLUMN --}} +{{--
    --}} + +{{-- --}} + +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}}{{-- NOW LABEL --}} +{{--
    --}} +{{-- {{ $now->format('H:i') }}--}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- DAYS --}} +{{-- @foreach($this->calendarDays as $day)--}} + +{{-- --}} + +{{-- --}}{{-- HEADER --}} +{{-- --}} +{{--
    --}} +{{-- {{ $day['date']->translatedFormat('D') }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $day['date']->format('d') }}--}} +{{--
    --}} +{{--
    --}} + +{{-- --}}{{-- ALL DAY --}} +{{-- --}}{{-- @if($day['allDay']->isNotEmpty())--}} +{{-- --}}{{--
    --}} +{{-- --}}{{-- @foreach($day['allDay'] as $event)--}} + +{{-- --}}{{-- --}} +{{-- --}}{{-- {{ $event->title }}--}} +{{-- --}}{{--
    --}} + +{{-- --}}{{-- @endforeach--}} +{{-- --}}{{--
    --}} +{{-- --}}{{-- @endif--}} +{{-- --}}{{-- GRID --}} +{{--
    --}} +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}} +{{-- --}}{{-- EVENTS --}} +{{-- @foreach($day['timed'] as $event)--}} + +{{-- --}} + +{{-- --}}{{-- RESIZE TOP --}} +{{--
    --}} + +{{-- --}}{{-- CONTENT --}} +{{--
    --}} +{{--
    --}} +{{-- {{ $event['title'] }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $event['start']->format('H:i') }}--}} +{{-- @if($event['end'])--}} +{{-- – {{ $event['end']->format('H:i') }}--}} +{{-- @endif--}} +{{--
    --}} +{{--
    --}} + +{{-- --}}{{-- RESIZE BOTTOM --}} +{{--
    --}} + +{{-- --}} + +{{-- @endforeach--}} + +{{-- --}} +{{-- --}} + +{{-- @endforeach--}} + +{{-- --}} + +{{-- --}}{{-- NOW LINE --}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} +{{--
    --}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} + +{{-- --}} +{{----}} + + +{{-- @foreach($this->calendarDays as $day)--}} + +{{-- --}} + +{{-- --}}{{-- HEADER --}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $day['date']->translatedFormat('D') }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $day['date']->format('d') }}--}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- ALL DAY EVENTS --}} +{{-- @if($day['allDay']->isNotEmpty())--}} +{{--
    --}} +{{-- @foreach($day['allDay'] as $event)--}} +{{--
    --}} +{{-- {{ $event->title }}--}} +{{--
    --}} +{{-- @endforeach--}} +{{--
    --}} +{{-- @endif--}} + +{{-- --}}{{-- GRID + EVENTS --}} +{{--
    --}} +{{-- --}}{{-- GRID --}} +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}}{{-- EVENTS --}} +{{-- @foreach($day['timed'] as $event)--}} + +{{-- --}} + +{{-- --}}{{-- RESIZE TOP --}} +{{--
    --}} + +{{--
    --}} +{{--
    --}} +{{-- {{ $event['title'] }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $event['start']->format('H:i') }}--}} +{{-- @if($event['end'])--}} +{{-- – {{ $event['end']->format('H:i') }}--}} +{{-- @endif--}} +{{--
    --}} +{{--
    --}} + +{{-- --}}{{-- RESIZE BOTTOM --}} +{{--
    --}} + +{{--
    --}} + +{{-- @endforeach--}} + + +{{--
    --}} +{{-- --}} +{{-- --}} +{{-- @endforeach--}} + +{{--@php--}} +{{-- $tz = auth()->user()->timezone;--}} +{{-- $now = now($tz);--}} + +{{-- $hourHeight = 64;--}} +{{-- $nowTop = ($now->hour * $hourHeight) + (($now->minute / 60) * $hourHeight);--}} +{{--@endphp--}} + +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- TIME COLUMN --}} +{{--
    --}} + +{{--
    --}} + +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- {{ str_pad($h, 2, '0', STR_PAD_LEFT) }}:00--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}}{{-- NOW LABEL --}} +{{--
    --}} +{{-- {{ $now->format('H:i') }}--}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- DAYS --}} +{{-- @foreach($this->calendarDays as $day)--}} + +{{-- --}} + +{{-- --}}{{-- HEADER --}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $day['date']->translatedFormat('D') }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $day['date']->format('d') }}--}} +{{--
    --}} + +{{--
    --}} + +{{-- --}}{{-- ALL DAY EVENTS --}} +{{-- @if($day['allDay']->isNotEmpty())--}} +{{--
    --}} +{{-- @foreach($day['allDay'] as $event)--}} +{{--
    --}} +{{-- {{ $event->title }}--}} +{{--
    --}} +{{-- @endforeach--}} +{{--
    --}} +{{-- @endif--}} + +{{-- --}}{{-- GRID + EVENTS --}} +{{--
    --}} +{{-- --}}{{-- GRID --}} +{{-- @for($h = 0; $h < 24; $h++)--}} +{{--
    --}} +{{-- @endfor--}} + +{{-- --}}{{-- EVENTS --}} +{{-- @foreach($day['timed'] as $event)--}} + +{{-- --}} +{{-- --}}{{-- πŸ”₯ RESIZE TOP --}} +{{--
    --}} + +{{-- --}}{{-- CONTENT --}} +{{--
    --}} +{{--
    --}} +{{-- {{ $event['title'] }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $event['start']->format('H:i') }}--}} +{{-- @if($event['end'])--}} +{{-- – {{ $event['end']->format('H:i') }}--}} +{{-- @endif--}} +{{--
    --}} +{{--
    --}} + +{{-- --}}{{-- πŸ”₯ RESIZE BOTTOM --}} +{{--
    --}} + +{{--
    --}} + +{{-- @endforeach--}} + +{{--
    --}} +{{-- --}} +{{-- --}} +{{-- @endforeach--}} +{{-- --}} + +{{-- --}}{{-- NOW LINE --}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} +{{--
    --}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} + +{{----}} + +{{-- + +{{-- NOW LINE AUTO UPDATE --}} + diff --git a/src/resources/views/livewire/calendar/week-canvas.blade.php b/src/resources/views/livewire/calendar/week-canvas.blade.php new file mode 100644 index 0000000..4bbac26 --- /dev/null +++ b/src/resources/views/livewire/calendar/week-canvas.blade.php @@ -0,0 +1,231 @@ +@php + $totalHours = $hourEnd - $hourStart; // 24 + $cellPx = 48; // px pro Stunde (muss mit CELL_PX in calendar-week-canvas.js ΓΌbereinstimmen) + $totalPx = $totalHours * $cellPx; + $days = [t('calendar.days.mo'), t('calendar.days.di'), t('calendar.days.mi'), t('calendar.days.do'), t('calendar.days.fr'), t('calendar.days.sa'), t('calendar.days.so')]; + $tz = auth()->user()->timezone; + $today = now($tz)->toDateString(); + $nowLocal = now($tz); + + // Named-Color-Palette (fΓΌr KompatibilitΓ€t mit bestehenden EintrΓ€gen) + $palette = [ + 'indigo' => ['#EEF2FF', '#4F46E5', '#3730A3'], + 'green' => ['#ECFDF5', '#10B981', '#065F46'], + 'amber' => ['#FFFBEB', '#F59E0B', '#92400E'], + 'red' => ['#FEF2F2', '#EF4444', '#991B1B'], + 'blue' => ['#EFF6FF', '#3B82F6', '#1E40AF'], + 'purple' => ['#FAF5FF', '#9333EA', '#6B21A8'], + ]; + + /** + * Liefert [$bg, $border, $text] fΓΌr beliebige Farb-Werte: + * - Named key ('indigo', 'green', …) β†’ Palette + * - Hex-String ('#4F46E5', '#abc') β†’ generierte RGBA-TΓΆne + * - null / unbekannt β†’ Primary-Fallback (#4F46E5) + */ + $resolveColor = function (?string $color) use ($palette): array { + if ($color && isset($palette[$color])) { + return $palette[$color]; + } + + // Hex-Farbe (#rgb oder #rrggbb) + if ($color && preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $color)) { + $hex = ltrim($color, '#'); + if (strlen($hex) === 3) { + $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; + } + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + + // Solides Pastell: 18 % Farbe + 82 % Weiß β†’ deckt dahinterliegende Events vollstΓ€ndig ab + $bgR = (int) round(255 - (255 - $r) * 0.18); + $bgG = (int) round(255 - (255 - $g) * 0.18); + $bgB = (int) round(255 - (255 - $b) * 0.18); + + return [ + "rgb($bgR,$bgG,$bgB)", // solider Pastell-Hintergrund + $color, // Border: die Farbe selbst + $color, // Text: die Farbe selbst + ]; + } + + // Default: Primary (passend zum Design-System) + return ['rgb(236,237,253)', '#4F46E5', '#3730A3']; + }; +@endphp + +
    + + {{-- ── Header ── --}} +
    +
    + + + {{ $this->titleString }} + + +
    +
    + +
    + @foreach(['month' => t('calendar.month'), 'week' => t('calendar.week'), 'day' => t('calendar.day')] as $v => $label) + + @endforeach +
    +
    +
    + + {{-- ── Skeleton (sichtbar wΓ€hrend Livewire-Request) ── --}} +
    + @if($view === 'week') + @include('livewire.calendar.skeletons.week') + @elseif($view === 'day') + @include('livewire.calendar.skeletons.day') + @else + @include('livewire.calendar.skeletons.month') + @endif +
    + + {{-- ── Echter Inhalt (ausgeblendet wΓ€hrend Livewire-Request) ── --}} +
    + + {{-- ── Tagesspalten-Header (nur Wochenansicht) ── --}} + @if($view === 'week') +
    +
    + @foreach($weekDays as $i => $day) +
    +
    {{ $days[$i] }}
    +
    + {{ $day->day }} +
    +
    + @endforeach +
    + @endif + + {{-- ── GanztΓ€gige Events (nur Wochenansicht) ── --}} + @if($view === 'week' && $hasAllDay) +
    + + {{-- "ganzt." Label --}} +
    + {{ t('calendar.all_day') }} +
    + + {{-- Einziger Container ΓΌber alle 7 Spalten – Events absolute darin --}} +
    + + {{-- Spalten-Trennlinien als Hintergrund --}} + @foreach($weekDays as $colIndex => $day) +
    + @endforeach + + {{-- Drop-Preview Highlight --}} + + + {{-- All-Day Events – jeder als einzelner Block --}} + @foreach($allDayRows as $allDayEvent) + @php + [$bg, $border, $text] = $resolveColor($allDayEvent['color']); + $leftPct = ($allDayEvent['startCol'] / 7) * 100; + $widthPct = (($allDayEvent['endCol'] - $allDayEvent['startCol'] + 1) / 7) * 100; + $topPx = $allDayEvent['row'] * 26 + 4; + + // Abgerundete Ecken: links wenn Start in dieser Woche, rechts wenn Ende in dieser Woche + $rLeft = $allDayEvent['startsInWeek'] ? '4px' : '0'; + $rRight = $allDayEvent['endsInWeek'] ? '4px' : '0'; + @endphp + +
    + + @if(!$allDayEvent['startsInWeek']) + β€Ή + @endif + + + @if($allDayEvent['startTime']) + {{ $allDayEvent['startTime'] }}–{{ $allDayEvent['endTime'] }} + @endif + {{ $allDayEvent['title'] }} + + + @if(!$allDayEvent['endsInWeek']) + β€Ί + @endif +
    + @endforeach + +
    +
    + @endif + + + {{-- ── Scroll-Bereich (nur timed Events) ── --}} +
    + + {{-- VIEW --}} + @if($view === 'month') + @include('livewire.calendar.views.month-canvas') + @endif + + @if($view === 'week') + @include('livewire.calendar.views.week-canvas') + @endif + + @if($view === 'day') + @include('livewire.calendar.views.day-canvas') + @endif +
    + +
    + + {{-- SIDEBAR --}} + @livewire('calendar.sidebar') + + +
    diff --git a/src/resources/views/livewire/checkout/index.blade.php b/src/resources/views/livewire/checkout/index.blade.php new file mode 100644 index 0000000..aed0c9a --- /dev/null +++ b/src/resources/views/livewire/checkout/index.blade.php @@ -0,0 +1,341 @@ +
    + + {{-- HEADER --}} +
    + + + +
    + @if($this->checkoutType === 'cancel') +

    {{ t('checkout.switch_free') }}

    +

    {{ t('checkout.cancel_immediately') }}

    + @elseif($this->checkoutType === 'update') +

    {{ t('checkout.change_plan') }}

    +

    {{ t('checkout.switch_to') }} {{ $plan->name }} – {{ t('checkout.prorated') }}

    + @else +

    {{ t('checkout.title') }}

    +

    {{ t('checkout.switch_to') }} {{ $plan->name }}

    + @endif +
    +
    + + +
    + + {{-- ============================================================ + LEFT β€” ORDER SUMMARY + ============================================================ --}} +
    + + {{-- BILLING TOGGLE (nur bei paid plans) --}} + @if($this->checkoutType !== 'cancel') +
    + +
    {{ t('checkout.billing_period') }}
    + +
    + + {{-- Monthly --}} + + + {{-- Yearly --}} + + +
    + + @if($billing === 'yearly' && $this->savings() > 0) +
    + + + {{ t('checkout.save_comparison', ['amount' => number_format($this->savings() / 100, 2, ',', '.') . ' €']) }} + +
    + @endif + +
    + @endif {{-- end billing toggle --}} + + + {{-- FEATURES --}} +
    + +
    {{ t('checkout.included', ['plan' => $plan->name]) }}
    + + @if($features->isEmpty()) +

    {{ t('checkout.all_features') }}

    + @else +
    + @foreach($features as $feature) +
    +
    + +
    + {{ $feature->label }} +
    + @endforeach +
    + @endif + + @if($plan->credit_limit) +
    + + + {{ number_format($plan->credit_limit) }} + {{ t('checkout.credits_month') }} + +
    + @endif + +
    + + + {{-- TRUST SIGNALS --}} +
    + +
    + @foreach([ + ['icon' => 'shield-check', 'title' => t('checkout.secure_payment'), 'desc' => t('checkout.ssl_stripe')], + ['icon' => 'arrow-uturn-left', 'title' => t('checkout.cancel_anytime'), 'desc' => t('checkout.no_minimum')], + ['icon' => 'lock-closed', 'title' => t('checkout.gdpr'), 'desc' => t('checkout.eu_data')], + ] as $trust) +
    +
    + @if($trust['icon'] === 'shield-check') + @elseif($trust['icon'] === 'arrow-uturn-left') + @else + @endif +
    +
    +

    {{ $trust['title'] }}

    +

    {{ $trust['desc'] }}

    +
    +
    + @endforeach +
    + + {{-- Stripe badge --}} +
    + + + + {{ t('checkout.stripe_note') }} +
    + +
    + +
    + + + {{-- ============================================================ + RIGHT β€” PRICE SUMMARY + PAY BUTTON + ============================================================ --}} +
    + + {{-- Summary Card --}} +
    + +
    +
    +
    +

    Plan

    +

    {{ $plan->name }}

    +
    +
    +

    {{ t('checkout.total') }}

    +

    + {{ number_format($this->activePrice() / 100, 2, ',', '.') }} € +

    +

    + / {{ $billing === 'yearly' ? t('common.year') : t('common.month') }} +

    +
    +
    +
    + +
    + + {{-- Line items --}} +
    + +
    + {{ $plan->name }} – {{ $billing === 'yearly' ? t('checkout.yearly') : t('checkout.monthly') }} + + {{ number_format($this->activePrice() / 100, 2, ',', '.') }} € + +
    + + @if($billing === 'yearly' && $this->savings() > 0) +
    + {{ t('checkout.yearly_discount') }} + + βˆ’{{ number_format($this->savings() / 100, 2, ',', '.') }} € + +
    + @endif + +
    + {{ t('checkout.total_incl_vat') }} + {{ number_format($this->activePrice() / 100, 2, ',', '.') }} € +
    +
    + + {{-- Renewal info --}} +
    + + {{ t('checkout.auto_renew', ['period' => $billing === 'yearly' ? t('checkout.period.12months') : t('checkout.period.30days')]) }} + {{ t('checkout.cancel_anytime') }} +
    + + {{-- Info-Hinweis je nach Typ --}} + @if($this->checkoutType === 'cancel') +
    +
    + + {{ t('checkout.cancel_immediate') }} +
    +

    {{ t('checkout.cancel_desc') }}

    +
    + @elseif($this->checkoutType === 'update') +
    +
    + + {{ t('checkout.switch_immediate') }} +
    +

    {{ t('checkout.switch_desc') }}

    +
    + @endif + + {{-- CTA --}} + @if($this->checkoutType === 'cancel') + + @elseif($this->checkoutType === 'update') + + @else + + @endif + +

    + @if($this->checkoutType === 'new') + {{ t('checkout.redirect_stripe') }} + @elseif($this->checkoutType === 'update') + {{ t('checkout.no_checkout') }} + @else + {{ t('checkout.cancel_effective') }} + @endif +

    + +
    +
    + + {{-- Current user info --}} +
    +
    + {{ strtoupper(substr(auth()->user()->name, 0, 1)) }} +
    +
    +

    {{ auth()->user()->name }}

    +

    {{ auth()->user()->email }}

    +
    +
    + +
    + +
    + +
    diff --git a/src/resources/views/livewire/checkout/success.blade.php b/src/resources/views/livewire/checkout/success.blade.php new file mode 100644 index 0000000..4ed89fe --- /dev/null +++ b/src/resources/views/livewire/checkout/success.blade.php @@ -0,0 +1,155 @@ +
    + + {{-- ============================================================ + PROCESSING STATE β€” solange noch nicht fertig + ============================================================ --}} + @if(!$done) + + {{-- Spinner Icon --}} +
    +
    + + + + +
    +
    + +
    +

    {{ t('checkout.success.processing') }}

    +

    {{ t('checkout.success.wait') }}

    +
    + + {{-- Step list --}} +
    + + {{-- Step 1: Subscription --}} +
    + @if($subscriptionReady) +
    + +
    + @else +
    + + + + +
    + @endif +
    +

    + {{ t('checkout.success.activate_sub') }} +

    +

    + {{ $subscriptionReady ? t('checkout.success.sub_created') : t('checkout.success.waiting_stripe') }} +

    +
    + @if($subscriptionReady) + {{ t('checkout.success.done') }} + @endif +
    + + {{-- Step 2: Payment --}} +
    + @if($paymentReady) +
    + +
    + @elseif($subscriptionReady) +
    + + + + +
    + @else +
    + +
    + @endif +
    +

    + {{ t('checkout.success.capture') }} +

    +

    + {{ $paymentReady ? t('checkout.success.payment_saved') : t('checkout.success.confirming') }} +

    +
    + @if($paymentReady) + {{ t('checkout.success.done') }} + @endif +
    + + {{-- Step 3: Fertig --}} +
    +
    + +
    +
    +

    {{ t('checkout.success.unlock_plan') }}

    +

    {{ t('checkout.success.features_on') }}

    +
    +
    + +
    + + @else + + {{-- ============================================================ + SUCCESS STATE β€” alles fertig + ============================================================ --}} + + {{-- Check Icon --}} +
    +
    + +
    +
    + +
    +

    {{ t('checkout.success.all_ready') }}

    +

    + {{ t('checkout.success.sub_active') }} +

    +
    + + {{-- Completed steps summary --}} +
    + + @foreach([ + ['label' => t('checkout.success.sub_activated'), 'desc' => t('checkout.success.plan_created')], + ['label' => t('checkout.success.payment_captured'), 'desc' => t('checkout.success.invoice_hint')], + ['label' => t('checkout.success.plan_unlocked'), 'desc' => t('checkout.success.all_features')], + ] as $step) +
    +
    + +
    +
    +

    {{ $step['label'] }}

    +

    {{ $step['desc'] }}

    +
    + {{ t('checkout.success.done') }} +
    + @endforeach + +
    + + {{-- Actions --}} + + + @endif + +
    diff --git a/src/resources/views/livewire/contacts/index.blade.php b/src/resources/views/livewire/contacts/index.blade.php new file mode 100644 index 0000000..bc041d6 --- /dev/null +++ b/src/resources/views/livewire/contacts/index.blade.php @@ -0,0 +1,332 @@ +
    + + {{-- SLIDE-IN PANEL --}} +
    + + {{-- Backdrop --}} +
    + + {{-- Panel --}} +
    + + {{-- Panel Header --}} +
    +

    + {{ $editId ? t('contacts.edit') : t('contacts.new') }} +

    + +
    + + {{-- Formular --}} +
    + + {{-- Name --}} +
    + + + @error('name')

    {{ $message }}

    @enderror +
    + + {{-- E-Mail --}} +
    + +
    + + +
    + @error('email')

    {{ $message }}

    @enderror +
    + + {{-- Telefon --}} +
    + +
    + + +
    +
    + + {{-- Typ --}} +
    + +
    + @foreach(['privat' => t('contacts.cat_private'), 'arbeit' => t('contacts.cat_work'), 'kunde' => t('contacts.cat_customer'), 'sonstiges' => t('contacts.cat_other')] as $key => $label) + + @endforeach +
    +
    + + {{-- Geburtstag --}} +
    + +
    + + +
    + @error('birthday')

    {{ $message }}

    @enderror +
    + + {{-- Notizen --}} +
    + + +
    + +
    + + {{-- Panel Footer --}} +
    + + +
    + +
    +
    + + + {{-- HEADER --}} +
    +
    +

    {{ t('contacts.title') }}

    +

    {{ t('contacts.subtitle') }}

    +
    + +
    + + + {{-- STATS --}} + @if($stats && $stats->total > 0) +
    + @foreach([ + ['label' => t('common.total'), 'value' => $stats->total, 'color' => 'text-gray-600', 'bg' => 'bg-gray-100', 'filter' => ''], + ['label' => t('contacts.cat_private'), 'value' => $stats->privat, 'color' => 'text-blue-600', 'bg' => 'bg-blue-50', 'filter' => 'privat'], + ['label' => t('contacts.cat_work'), 'value' => $stats->arbeit, 'color' => 'text-indigo-600', 'bg' => 'bg-indigo-50', 'filter' => 'arbeit'], + ['label' => t('contacts.customers'), 'value' => $stats->kunde, 'color' => 'text-green-600', 'bg' => 'bg-green-50', 'filter' => 'kunde'], + ] as $stat) + + @endforeach +
    + @endif + + + {{-- SUCHE + FILTER --}} +
    + +
    + + +
    + +
    + @foreach(['' => t('common.all'), 'privat' => t('contacts.cat_private'), 'arbeit' => t('contacts.cat_work'), 'kunde' => t('contacts.cat_customer'), 'sonstiges' => t('contacts.cat_other')] as $key => $label) + + @endforeach +
    + +
    + + + {{-- KONTAKTLISTE --}} + @if($contacts->isEmpty()) + +
    +
    + +
    +

    {{ t('contacts.no_contacts') }}

    + @if($search || $filterType) + + @else + + @endif +
    + + @else + +
    + @foreach($contacts as $contact) + @php $c = $contact->typeClasses(); @endphp + +
    + + {{-- Avatar --}} +
    + {{ $contact->initials() }} +
    + + {{-- Info --}} +
    +
    +

    {{ $contact->name }}

    + @if($contact->type) + + {{ $contact->typeLabel() }} + + @endif +
    + +
    + @if($contact->email) + + + {{ $contact->email }} + + @endif + @if($contact->phone) + + + {{ $contact->phone }} + + @endif +
    + +
    + @if($contact->birthday) + + + {{ $contact->birthday->format('d.m.Y') }} + + @endif + @if($contact->notes) + {{ $contact->notes }} + @endif +
    +
    + + {{-- Datum --}} + + + {{-- Aktionen (hover) --}} +
    + + +
    + +
    + @endforeach +
    + + {{-- PAGINATION --}} + @if($contacts->hasPages()) +
    + + {{ $contacts->firstItem() }}–{{ $contacts->lastItem() }} {{ t('contacts.of_total', ['total' => $contacts->total()]) }} + +
    + @if($contacts->onFirstPage()) + β€Ή + @else + + @endif + + @foreach($contacts->getUrlRange(max(1, $contacts->currentPage() - 2), min($contacts->lastPage(), $contacts->currentPage() + 2)) as $page => $url) + + @endforeach + + @if($contacts->hasMorePages()) + + @else + β€Ί + @endif +
    +
    + @endif + + @endif + +
    diff --git a/src/resources/views/livewire/dashboard/index.blade.php b/src/resources/views/livewire/dashboard/index.blade.php new file mode 100644 index 0000000..e4e450a --- /dev/null +++ b/src/resources/views/livewire/dashboard/index.blade.php @@ -0,0 +1,429 @@ +
    + + {{-- HEADER --}} +
    +
    +

    + {{ $greeting }}, {{ $firstName }} +

    +

    + {{ now($tz)->translatedFormat('l, d. F Y') }} +

    +
    + +
    + @if($stats['overdue_tasks'] > 0) + + + {{ $stats['overdue_tasks'] }} {{ t('dashboard.overdue') }} + + @endif + + + {{ t('dashboard.open_calendar') }} + +
    +
    + + + {{-- STATS --}} +
    + @foreach([ + ['label' => t('dashboard.events_today'), 'value' => $stats['events_today'], 'icon' => 'calendar', 'color' => 'text-indigo-600', 'bg' => 'bg-indigo-50', 'href' => route('calendar.index')], + ['label' => t('dashboard.open_tasks'), 'value' => $stats['open_tasks'], 'icon' => 'check-circle', 'color' => 'text-teal-600', 'bg' => 'bg-teal-50', 'href' => route('tasks.index')], + ['label' => t('dashboard.notes'), 'value' => $stats['notes_total'], 'icon' => 'document-text', 'color' => 'text-amber-600', 'bg' => 'bg-amber-50', 'href' => route('notes.index')], + ] as $stat) + +
    + @if($stat['icon'] === 'calendar') + @elseif($stat['icon'] === 'check-circle') + @else + @endif +
    +
    +

    {{ number_format($stat['value']) }}

    +

    {{ $stat['label'] }}

    +
    +
    + @endforeach + + {{-- Credits Card --}} + @if(auth()->user()->isUnlimitedUser()) + +
    + +
    +
    + + + {{ t('dashboard.unlimited') }} + +

    {{ t('dashboard.credits_month') }}

    +
    +
    + @else + +
    + +
    +
    +

    {{ number_format($stats['credits_month']) }}

    +

    {{ t('dashboard.credits_month') }}

    +
    +
    + @endif +
    + + + {{-- GEBURTSTAGE --}} + @if($birthdaysToday->count() > 0) +
    +
    + +
    +
    + @foreach($birthdaysToday->take(3) as $c) +
    +
    + {{ strtoupper(substr($c->name, 0, 1)) }} +
    + {{ $c->name }} + @if(\Carbon\Carbon::parse($c->birthday)->year > 1900) + {{ now()->year - \Carbon\Carbon::parse($c->birthday)->year }} {{ t('common.age_short', [], $locale) }} + @endif +
    + @endforeach + @if($birthdaysToday->count() > 3) + {{ t('dashboard.birthday.more', ['count' => $birthdaysToday->count() - 3], $locale) }} + @endif +
    +
    + + {{ t('dashboard.birthday.today', [], $locale) }} +
    +
    + + @elseif($birthdaysSoon->count() > 0) +
    +
    + +
    +
    + @foreach($birthdaysSoon->take(2) as $c) + @php $days = (int) now()->diffInDays(\Carbon\Carbon::parse($c->birthday)->setYear(now()->year), false); @endphp + + {{ $c->name }} + {{ t($days === 1 ? 'dashboard.birthday.in_day' : 'dashboard.birthday.in_days', ['days' => $days], $locale) }} + + @if(!$loop->last && $birthdaysSoon->count() > 1) + Β· + @endif + @endforeach + @if($birthdaysSoon->count() > 2) + {{ t('dashboard.birthday.more', ['count' => $birthdaysSoon->count() - 2], $locale) }} + @endif +
    +
    + @endif + + {{-- MAIN GRID: Termine + Aufgaben | AktivitΓ€ten + Schnellzugriff --}} +
    + + {{-- LINKE SPALTE (2/3) --}} +
    + + {{-- HEUTE: TERMINE --}} +
    +
    +
    +
    + +
    + {{ t('dashboard.events_today') }} +
    +
    + @if($todayEvents->count() > 0) + {{ $todayEvents->count() }} + @endif + {{ t('common.all') }} +
    +
    + +
    + @forelse($todayEvents as $event) + @php + $startLocal = $event->starts_at->setTimezone($tz); + $endLocal = $event->ends_at?->setTimezone($tz); + $hex = $event->color ?? '#6366f1'; + $isMultiDay = $event->spansMultipleDays(); + $startsToday = $startLocal->toDateString() === now($tz)->toDateString(); + $endsToday = $endLocal?->toDateString() === now($tz)->toDateString(); + @endphp +
    +
    + + {{-- Zeit-Spalte --}} +
    + @if($event->is_all_day) + {{-- Ganztag: nur bei mehrtΓ€gig das Datum anzeigen --}} + @if($isMultiDay) +

    + {{ $startLocal->format('d.m.') }}
    – {{ $endLocal?->format('d.m.') }} +

    + @endif + @else +

    {{ $startLocal->format('H:i') }}

    + @if($endLocal && $endsToday) +

    {{ $endLocal->format('H:i') }}

    + @elseif($endLocal && !$endsToday) +

    β†’ {{ $endLocal->format('d.m.') }}

    + @endif + @endif +
    + +
    +

    {{ $event->title }}

    + @if($event->notes) +

    {{ $event->notes }}

    + @endif +
    + + {{-- Badge --}} + @if($event->is_all_day) + + {{ $isMultiDay ? t('dashboard.multiday') : t('dashboard.all_day') }} + + @elseif($isMultiDay) + {{ t('dashboard.multiday') }} + @endif +
    + @empty +
    + +

    {{ t('dashboard.no_events') }}

    + {{ t('dashboard.create_event') }} +
    + @endforelse +
    + + {{-- Next Events (wenn heute leer) --}} + @if($todayEvents->isEmpty() && $upcomingEvents->count() > 0) +
    +

    {{ t('dashboard.next_events') }}

    + @foreach($upcomingEvents as $event) +
    + +

    {{ $event->title }}

    + + {{ $event->starts_at->setTimezone($tz)->format('d.m. H:i') }} + +
    + @endforeach +
    + @endif +
    + + + {{-- OFFENE AUFGABEN --}} +
    +
    +
    +
    + +
    + {{ t('dashboard.open_tasks') }} +
    +
    + @if($stats['open_tasks'] > 0) + {{ $stats['open_tasks'] }} + @endif + {{ t('common.all') }} +
    +
    + +
    + @forelse($openTasks as $task) + @php $p = $task->priorityClasses(); @endphp +
    + +
    +

    + {{ $task->title }} +

    +
    + @php + $isOverdue = $task->due_at && $task->due_at->isPast(); + $hasReminder = $task->reminder_at + && $task->reminder_at->isFuture() + && !$task->reminder_sent; + @endphp +
    + @if($isOverdue) + + {{ $task->due_at->format('d.m.') }} + + + @elseif($task->due_at) + + {{ $task->due_at->format('d.m.') }} + + @endif + @if($hasReminder) + + + {{ $task->reminder_at->setTimezone(auth()->user()->timezone ?? 'Europe/Vienna')->format('H:i') }} + + @endif +
    +
    + @empty +
    + +

    {{ t('dashboard.all_tasks_done') }}

    + {{ t('dashboard.new_task') }} +
    + @endforelse +
    +
    + +
    + + + {{-- RECHTE SPALTE (1/3) --}} +
    + + {{-- PLAN + CREDITS --}} +
    +
    + {{ $plan?->name ?? 'Free' }} + @if($subscription?->ends_at) + {{ t('common.until') }} {{ $subscription->ends_at->format('d.m.Y') }} + @else + {{ t('common.active') }} + @endif +
    + + @if(auth()->user()->isUnlimitedUser()) +
    + + + {{ t('dashboard.credits') }} + + + + {{ t('dashboard.unlimited') }} + +
    + @elseif($creditLimit > 0) +
    +
    + + + {{ t('dashboard.credits_this_month') }} + + {{ number_format($stats['credits_month']) }} / {{ number_format($creditLimit) }} +
    +
    +
    +
    +
    + @else +

    {{ t('dashboard.no_credit_limit') }}

    + @endif + + @if(!auth()->user()->isInternalUser()) + + {{ t('dashboard.manage_plan') }} + + @endif +
    + + + {{-- LETZTE AKTIVITΓ„TEN --}} +
    +
    + {{ t('dashboard.activities') }} + {{ t('common.all') }} +
    + +
    + @forelse($recentActivities as $activity) + @php $v = $activity->visual(); @endphp +
    +
    + @if($v['icon'] === 'calendar') + @elseif($v['icon'] === 'bell') + @elseif($v['icon'] === 'bolt') + @elseif($v['icon'] === 'user') + @elseif($v['icon'] === 'document-text') + @elseif($v['icon'] === 'check-circle') + @elseif($v['icon'] === 'link') + @else + @endif +
    +
    +

    {{ $activity->title }}

    +

    {{ $activity->created_at->diffForHumans() }}

    +
    +
    + @empty +

    {{ t('dashboard.no_activities') }}

    + @endforelse +
    +
    + + + {{-- LETZTE NOTIZEN --}} + @if($recentNotes->count() > 0) +
    +
    + {{ t('dashboard.notes') }} + {{ t('common.all') }} +
    +
    + @foreach($recentNotes as $note) + @php $c = $note->colorClasses(); @endphp +
    + @if($note->title) +

    {{ $note->title }}

    + @endif +

    {{ $note->content }}

    +
    + @endforeach +
    +
    + @endif + +
    + +
    + + + {{-- ASSISTENT CTA --}} +
    +
    + +
    +
    +

    {{ t('dashboard.ai_assistant') }}

    +

    {{ t('dashboard.ai_subtitle') }}

    +
    + + + {{ t('dashboard.open_assistant') }} + +
    + +
    diff --git a/src/resources/views/livewire/homepage/agb.blade.php b/src/resources/views/livewire/homepage/agb.blade.php new file mode 100644 index 0000000..f7ff0c5 --- /dev/null +++ b/src/resources/views/livewire/homepage/agb.blade.php @@ -0,0 +1,163 @@ +
    + + @include('partials.homepage.navbar') + + {{-- ============================================================ + HEADER + ============================================================ --}} +
    +
    + +
    +
    + + Nutzungsbedingungen +
    +

    Allgemeine GeschΓ€ftsbedingungen

    +

    Zuletzt aktualisiert: {{ now()->format('d.m.Y') }}

    +
    +
    + + + {{-- ============================================================ + CONTENT + ============================================================ --}} +
    +
    +
    + + {{-- 1. Geltungsbereich --}} +
    +

    + 1 + Geltungsbereich +

    +

    + Diese Allgemeinen GeschΓ€ftsbedingungen (AGB) gelten fΓΌr die Nutzung der von der aziros GmbH betriebenen SaaS-Plattform β€žaziros" (nachfolgend β€žDienst"). Mit der Registrierung erkennst du diese AGB an. +

    +
    + + {{-- 2. Leistungsbeschreibung --}} +
    +

    + 2 + Leistungsbeschreibung +

    +

    + aziros ist eine webbasierte ProduktivitΓ€tsplattform, die folgende Funktionen bereitstellt: +

    +
      + @foreach([ + 'Kalender- und Terminverwaltung mit KI-UnterstΓΌtzung', + 'Aufgaben- und Notizverwaltung', + 'KI-Assistent fΓΌr natΓΌrlichsprachliche Terminplanung', + 'Kontaktverwaltung', + 'Synchronisation mit Google Calendar und Outlook', + 'Automatisierungsregeln fΓΌr wiederkehrende AblΓ€ufe', + ] as $item) +
    • + + {{ $item }} +
    • + @endforeach +
    +

    + Der Funktionsumfang richtet sich nach dem gewΓ€hlten Plan (Free, Pro, etc.). +

    +
    + + {{-- 3. Registrierung --}} +
    +

    + 3 + Registrierung & Benutzerkonto +

    +

    + Für die Nutzung des Dienstes ist eine Registrierung erforderlich. Du bist verpflichtet, wahrheitsgemÀße Angaben zu machen und deine Zugangsdaten vertraulich zu behandeln. Die Weitergabe deines Kontos an Dritte ist nicht gestattet. +

    +
    + + {{-- 4. Preise & Zahlung --}} +
    +

    + 4 + Preise & Zahlung +

    +

    + Die aktuellen Preise sind auf unserer Preisseite einsehbar. Alle Preise verstehen sich inklusive der gesetzlichen Mehrwertsteuer. Die Abrechnung erfolgt je nach gewΓ€hltem Abrechnungszeitraum monatlich oder jΓ€hrlich im Voraus. +

    +
    + + {{-- 5. KΓΌndigung --}} +
    +

    + 5 + Laufzeit & KΓΌndigung +

    +

    + Kostenpflichtige PlΓ€ne kΓΆnnen jederzeit zum Ende der aktuellen Abrechnungsperiode gekΓΌndigt werden. Der kostenlose Plan kann jederzeit durch LΓΆschung des Benutzerkontos beendet werden. Nach KΓΌndigung bleiben deine Daten noch 30 Tage erhalten, bevor sie endgΓΌltig gelΓΆscht werden. +

    +
    + + {{-- 6. Nutzungsbedingungen --}} +
    +

    + 6 + Nutzungsbedingungen +

    +

    Du verpflichtest dich, den Dienst nicht missbrΓ€uchlich zu nutzen. Insbesondere ist es untersagt:

    +
      + @foreach([ + 'Den Dienst für rechtswidrige Zwecke zu verwenden', + 'Die Infrastruktur durch automatisierte Massenanfragen zu überlasten', + 'Sicherheitsmechanismen zu umgehen oder zu manipulieren', + 'Inhalte zu speichern, die gegen geltendes Recht verstoßen', + ] as $item) +
    • + + {{ $item }} +
    • + @endforeach +
    +
    + + {{-- 7. Haftung --}} +
    +

    + 7 + HaftungsbeschrΓ€nkung +

    +

    + Die Haftung der aziros GmbH ist auf Vorsatz und grobe FahrlΓ€ssigkeit beschrΓ€nkt. FΓΌr die VerfΓΌgbarkeit des Dienstes ΓΌbernehmen wir keine Garantie, bemΓΌhen uns aber um eine Uptime von 99,9%. FΓΌr Datenverluste, die durch fehlende Sicherungen deinerseits entstehen, haften wir nicht. +

    +
    + + {{-- 8. Datenschutz --}} +
    +

    + 8 + Datenschutz +

    +

    + Informationen zur Verarbeitung personenbezogener Daten findest du in unserer DatenschutzerklΓ€rung. +

    +
    + + {{-- 9. Schlussbestimmungen --}} +
    +

    + 9 + Schlussbestimmungen +

    +

    + Es gilt das Recht der Republik Γ–sterreich. Gerichtsstand ist Wien, sofern gesetzlich zulΓ€ssig. Sollte eine Bestimmung dieser AGB unwirksam sein, bleiben die ΓΌbrigen Bestimmungen davon unberΓΌhrt. Die unwirksame Bestimmung wird durch eine wirksame ersetzt, die dem wirtschaftlichen Zweck am nΓ€chsten kommt. +

    +
    + +
    +
    +
    + + @include('partials.homepage.footer') + +
    diff --git a/src/resources/views/livewire/homepage/datenschutz.blade.php b/src/resources/views/livewire/homepage/datenschutz.blade.php new file mode 100644 index 0000000..106d5d5 --- /dev/null +++ b/src/resources/views/livewire/homepage/datenschutz.blade.php @@ -0,0 +1,201 @@ +
    + + @include('partials.homepage.navbar') + + {{-- ============================================================ + HEADER + ============================================================ --}} +
    +
    + +
    +
    + + Deine Daten sind sicher +
    +

    DatenschutzerklΓ€rung

    +

    Zuletzt aktualisiert: {{ now()->format('d.m.Y') }}

    +
    +
    + + + {{-- ============================================================ + CONTENT + ============================================================ --}} +
    +
    +
    + + {{-- 1. Verantwortlicher --}} +
    +

    + 1 + Verantwortlicher +

    +

    + Verantwortlich fΓΌr die Datenverarbeitung auf dieser Website ist:

    + aziros GmbH
    + Musterstraße 1
    + 1010 Wien
    + Γ–sterreich

    + E-Mail: datenschutz@aziros.com
    + Telefon: +43 (0) 1 234567 +

    +
    + + {{-- 2. Erhobene Daten --}} +
    +

    + 2 + Welche Daten wir erheben +

    +

    Wir erheben und verarbeiten folgende personenbezogene Daten:

    +
      + @foreach([ + 'Bestandsdaten (z.B. Name, E-Mail-Adresse)', + 'Nutzungsdaten (z.B. besuchte Seiten, Zugriffszeit)', + 'Kalender- und Termindaten, die du in aziros erstellst', + 'Kontaktdaten, die du in deinem Profil hinterlegst', + 'Kommunikationsdaten bei Kontaktaufnahme (z.B. E-Mail-Inhalt)', + ] as $item) +
    • + + {{ $item }} +
    • + @endforeach +
    +
    + + {{-- 3. Zweck --}} +
    +

    + 3 + Zweck der Verarbeitung +

    +

    Deine Daten werden fΓΌr folgende Zwecke verarbeitet:

    +
      + @foreach([ + 'Bereitstellung und Betrieb der aziros-Plattform', + 'Verwaltung deines Benutzerkontos und deiner Abonnements', + 'Synchronisation mit externen Kalenderdiensten (Google, Outlook)', + 'Bereitstellung des KI-Assistenten zur Terminplanung', + 'Versand von E-Mail-Benachrichtigungen und Erinnerungen', + 'Analyse und Verbesserung unseres Angebots', + ] as $item) +
    • + + {{ $item }} +
    • + @endforeach +
    +
    + + {{-- 4. Rechtsgrundlage --}} +
    +

    + 4 + Rechtsgrundlage +

    +

    + Die Verarbeitung deiner Daten erfolgt auf Grundlage von Art. 6 Abs. 1 DSGVO: +

    +
      + @foreach([ + 'Einwilligung (Art. 6 Abs. 1 lit. a DSGVO) β€” z.B. bei der Registrierung', + 'VertragserfΓΌllung (Art. 6 Abs. 1 lit. b DSGVO) β€” fΓΌr die Nutzung der Plattform', + 'Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO) β€” fΓΌr Analyse und Verbesserung', + ] as $item) +
    • + + {{ $item }} +
    • + @endforeach +
    +
    + + {{-- 5. Speicherdauer --}} +
    +

    + 5 + Speicherdauer +

    +

    + Deine personenbezogenen Daten werden nur so lange gespeichert, wie es fΓΌr die oben genannten Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen. Nach LΓΆschung deines Accounts werden deine Daten innerhalb von 30 Tagen vollstΓ€ndig entfernt. +

    +
    + + {{-- 6. Drittanbieter --}} +
    +

    + 6 + Drittanbieter & DatenΓΌbermittlung +

    +

    + Wir nutzen folgende Drittanbieter zur Bereitstellung unserer Dienste: +

    +
    + @foreach([ + ['name' => 'Google Calendar API', 'desc' => 'FΓΌr die Synchronisation deines Google-Kalenders. Es gelten die Datenschutzbestimmungen von Google.'], + ['name' => 'Microsoft Graph API', 'desc' => 'FΓΌr die Synchronisation deines Outlook-Kalenders. Es gelten die Datenschutzbestimmungen von Microsoft.'], + ['name' => 'OpenAI', 'desc' => 'FΓΌr die KI-Assistenzfunktionen. Deine Eingaben werden verarbeitet, aber nicht zum Training der KI verwendet.'], + ] as $provider) +
    +

    {{ $provider['name'] }}

    +

    {{ $provider['desc'] }}

    +
    + @endforeach +
    +
    + + {{-- 7. Deine Rechte --}} +
    +

    + 7 + Deine Rechte +

    +

    Du hast jederzeit das Recht auf:

    +
    + @foreach([ + ['icon' => 'eye', 'title' => 'Auskunft', 'desc' => 'Welche Daten wir ΓΌber dich gespeichert haben'], + ['icon' => 'pencil-square', 'title' => 'Berichtigung', 'desc' => 'Korrektur unrichtiger Daten'], + ['icon' => 'trash', 'title' => 'LΓΆschung', 'desc' => 'LΓΆschung deiner personenbezogenen Daten'], + ['icon' => 'no-symbol', 'title' => 'Widerspruch', 'desc' => 'Widerspruch gegen die Verarbeitung'], + ] as $right) +
    +
    + @if($right['icon'] === 'eye') + @elseif($right['icon'] === 'pencil-square') + @elseif($right['icon'] === 'trash') + @else + @endif +
    +
    +

    {{ $right['title'] }}

    +

    {{ $right['desc'] }}

    +
    +
    + @endforeach +
    +

    + FΓΌr die AusΓΌbung deiner Rechte wende dich an datenschutz@aziros.com. +

    +
    + + {{-- 8. Cookies --}} +
    +

    + 8 + Cookies +

    +

    + Wir verwenden technisch notwendige Cookies, um die FunktionalitΓ€t der Plattform sicherzustellen (z.B. Session-Cookies fΓΌr die Anmeldung). Tracking-Cookies oder Cookies zu Werbezwecken werden nicht eingesetzt. +

    +
    + +
    +
    +
    + + @include('partials.homepage.footer') + +
    diff --git a/src/resources/views/livewire/homepage/example.blade.php b/src/resources/views/livewire/homepage/example.blade.php new file mode 100644 index 0000000..23de1b7 --- /dev/null +++ b/src/resources/views/livewire/homepage/example.blade.php @@ -0,0 +1,845 @@ +
    + + {{-- ============================================================ + NAVBAR + ============================================================ --}} + + + + {{-- ============================================================ + SECTION 1 β€” HERO + ============================================================ --}} +
    + + {{-- Background glows --}} +
    +
    +
    + +
    +
    + + {{-- LEFT: Copy --}} +
    + + {{-- Badge --}} +
    + + Beta Β· Jetzt kostenlos starten +
    + + {{-- Headline --}} +
    +

    + Dein KI-Assistent
    + + fΓΌr den Alltag + +

    +

    + Aria verwaltet deine Termine, Aufgaben und Kontakte β€” + einfach per Sprache oder Text. +

    +
    + + {{-- CTAs --}} + + + {{-- Founder note --}} +

    + Gebaut von einem Entwickler aus Wien Β· Keine Kreditkarte nΓΆtig +

    + + {{-- Trust badges --}} +
    + + πŸ‡¦πŸ‡Ή + Made in Austria + + + + DSGVO konform + + + + EU Server + + + + Keine Kreditkarte + +
    + +
    + + {{-- RIGHT: Kalender App Mock --}} +
    + + {{-- Floating badges --}} +
    +
    + +
    +
    +

    Termin erstellt

    +

    Meeting mit Jana – 14:00

    +
    +
    + +
    +
    + +
    +
    +

    Aria Assistent

    +

    "Morgen 10 Uhr frei?" β€” Ja

    +
    +
    + + {{-- Browser Frame --}} +
    + + {{-- Window bar --}} +
    +
    +
    +
    +
    +
    + app.aziros.com/calendar +
    +
    +
    + +
    + + {{-- Sidebar mock --}} +
    + @foreach([ + ['icon' => 'home', 'active' => false], + ['icon' => 'calendar', 'active' => true], + ['icon' => 'check-circle', 'active' => false], + ['icon' => 'document-text', 'active' => false], + ['icon' => 'users', 'active' => false], + ] as $item) +
    + @if($item['icon'] === 'home') + + @elseif($item['icon'] === 'calendar') + + @elseif($item['icon'] === 'check-circle') + + @elseif($item['icon'] === 'document-text') + + @else + + @endif +
    + @endforeach +
    + + {{-- Calendar mock --}} +
    + +
    + April 2026 +
    +
    +
    +
    +
    + + {{-- Day columns --}} +
    + @foreach(['Mo', 'Di', 'Mi', 'Do', 'Fr'] as $di => $day) +
    +
    {{ $day }}
    +
    + {{ 13 + $di }} +
    +
    + @endforeach +
    + + {{-- Events mock --}} +
    +
    +
    +

    Team-Stand

    +

    09:00–09:30

    +
    +
    +
    +
    +

    Arzt

    +

    10:00–11:00

    +
    +
    +

    Deep Work

    +

    14:00–16:00

    +
    +
    +
    +
    +
    +

    Design Review

    +

    11:00–12:00

    +
    +
    +
    +
    +

    Zahnarzt

    +

    09:30–10:30

    +
    +
    +
    + + {{-- Aria input mock --}} +
    + + Morgen Meeting mit Jana um 14 Uhr... +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + + + {{-- ============================================================ + SECTION 2 β€” FOUNDER STORY + ============================================================ --}} +
    +
    + +
    + + {{-- Left: Avatar --}} +
    +
    + BB +
    +
    +

    Boban Blaskovic

    +

    GrΓΌnder & Entwickler, Wien

    +
    +
    + + {{-- Right: Quote + Text --}} +
    +
    + "Ich hab Aziros gebaut weil ich selbst keinen Assistenten finden konnte der einfach funktioniert β€” + ohne Konfiguration, ohne Lernkurve. Einfach sagen was man braucht und es passiert." +
    +

    + Als Entwickler jongliere ich tΓ€glich zwischen Projekten, Terminen und Kundenkontakten. + Aziros ist mein eigenes Werkzeug β€” das ich jetzt mit dir teile. +

    +
    + +
    + +
    +
    + + + {{-- ============================================================ + SECTION 3 β€” WIE ES FUNKTIONIERT + ============================================================ --}} +
    +
    + +
    +

    So einfach ist Aziros

    +

    In drei Schritten produktiv

    +
    + +
    + + {{-- Connecting line --}} + + + @foreach([ + [ + 'step' => '1', + 'icon' => 'chat', + 'color' => 'indigo', + 'title' => 'Einfach sagen was du brauchst', + 'text' => 'Schreib oder sprich mit Aria β€” auf Deutsch, natΓΌrlich.', + ], + [ + 'step' => '2', + 'icon' => 'sparkles', + 'color' => 'purple', + 'title' => 'Aria versteht und handelt', + 'text' => 'Termine erstellen, verschieben, Aufgaben anlegen β€” alles automatisch.', + ], + [ + 'step' => '3', + 'icon' => 'check', + 'color' => 'teal', + 'title' => 'Du bleibst im Überblick', + 'text' => 'Kalender, Aufgaben und Kontakte immer aktuell und erreichbar.', + ], + ] as $s) +
    +
    +
    + @if($s['icon'] === 'chat') + @elseif($s['icon'] === 'sparkles') + @else + @endif +
    + {{ $s['step'] }} +
    +
    +
    +
    +

    {{ $s['title'] }}

    +

    {{ $s['text'] }}

    +
    +
    + @endforeach + +
    + +
    +
    + + + {{-- ============================================================ + SECTION 4 β€” FEATURES + ============================================================ --}} +
    +
    + +
    +
    + + Alle Werkzeuge in einer App +
    +

    + Alles was du brauchst +

    +

    + Von Terminen bis zu Automatisierungen β€” Aziros hat alles dabei. +

    +
    + +
    + @php + $features = [ + [ + 'icon' => 'calendar', + 'bg' => 'bg-indigo-50', + 'ic' => 'text-indigo-600', + 'tag' => 'bg-indigo-100 text-indigo-700', + 'pill' => 'Kalender', + 'title' => 'Kalender & Termine', + 'desc' => 'Erstelle und verwalte Termine per Sprache. Aria kennt deinen Kalender.', + ], + [ + 'icon' => 'clipboard', + 'bg' => 'bg-teal-50', + 'ic' => 'text-teal-600', + 'tag' => 'bg-teal-100 text-teal-700', + 'pill' => 'Aufgaben', + 'title' => 'Aufgaben & Notizen', + 'desc' => 'Behalte den Überblick ΓΌber alle offenen Punkte.', + ], + [ + 'icon' => 'microphone', + 'bg' => 'bg-purple-50', + 'ic' => 'text-purple-600', + 'tag' => 'bg-purple-100 text-purple-700', + 'pill' => 'KI', + 'title' => 'Aria Sprachassistent', + 'desc' => 'Sprich einfach β€” Aria versteht natΓΌrliche Sprache auf Deutsch.', + ], + [ + 'icon' => 'bolt', + 'bg' => 'bg-amber-50', + 'ic' => 'text-amber-600', + 'tag' => 'bg-amber-100 text-amber-700', + 'pill' => 'Automation', + 'title' => 'Automationen', + 'desc' => 'TΓ€gliche Agenda, Erinnerungen und mehr β€” vollautomatisch.', + ], + [ + 'icon' => 'users', + 'bg' => 'bg-rose-50', + 'ic' => 'text-rose-600', + 'tag' => 'bg-rose-100 text-rose-700', + 'pill' => 'Kontakte', + 'title' => 'Kontakte', + 'desc' => 'Alle wichtigen Personen an einem Ort, verbunden mit deinen Terminen.', + ], + [ + 'icon' => 'phone', + 'bg' => 'bg-green-50', + 'ic' => 'text-green-600', + 'tag' => 'bg-green-100 text-green-700', + 'pill' => 'Bald', + 'title' => 'Mobile App', + 'desc' => 'iOS und Android App β€” Aria immer dabei.', + ], + ]; + @endphp + + @foreach($features as $f) +
    +
    +
    + @if($f['icon'] === 'calendar') + @elseif($f['icon'] === 'clipboard') + @elseif($f['icon'] === 'microphone') + @elseif($f['icon'] === 'bolt') + @elseif($f['icon'] === 'users') + @else + @endif +
    + + {{ $f['pill'] }} + +
    +

    {{ $f['title'] }}

    +

    {{ $f['desc'] }}

    +
    + @endforeach + +
    + +
    +
    + + + {{-- ============================================================ + SECTION 5 β€” BETA ANGEBOT + ============================================================ --}} +
    + +
    +
    +
    +
    + +
    + +
    + + Limitiertes Beta-Angebot +
    + +

    + Sei einer der ersten
    100 Nutzer +

    + +

    + Als Beta-Nutzer bekommst du: direkten Draht zu mir als GrΓΌnder, + deine Feature-WΓΌnsche werden priorisiert und du hilfst Aziros besser zu machen. +

    + +
    + @foreach([ + 'Kostenlos starten β€” kein Risiko', + '500 Credits jeden Monat gratis', + 'Pro Plan 50% gΓΌnstiger als Beta-Nutzer', + ] as $point) +
    +
    + +
    + {{ $point }} +
    + @endforeach +
    + +
    + + Jetzt kostenlos testen + + +

    + Bereits {{ $userCount }} Nutzer dabei +

    +
    + +
    + +
    + + + {{-- ============================================================ + SECTION 6 β€” APP DOWNLOAD + ============================================================ --}} +
    +
    + +
    + + {{-- Left: Text + Email --}} +
    + +
    +
    + + Mobile App β€” coming soon +
    +

    + Aziros in
    + deiner Tasche +

    +

    + Die App fΓΌr iOS und Android kommt bald. Trag dich ein + und wir benachrichtigen dich beim Launch. +

    +
    + + {{-- Email Notify Form --}} + @if($notifySubmitted) +
    + +

    Danke! Wir melden uns beim App-Launch.

    +
    + @else +
    + + +
    + @error('notifyEmail') +

    {{ $message }}

    + @enderror + @endif + +
    + + {{-- Right: App Store Badges + Phone Mock --}} +
    + + {{-- iPhone Mockup --}} +
    +
    +
    +
    + 9:41 +
    +
    +
    + +
    + aziros +
    +
    +

    Heute Β· 17. April

    + @foreach([ + ['Team-Standup', '09:00', 'indigo'], + ['Design Review', '11:00', 'green'], + ['Kundencall', '14:00', 'purple'], + ['Zahnarzt', '16:00', 'amber'], + ] as $ev) +
    +

    {{ $ev[0] }}

    +

    {{ $ev[1] }}

    +
    + @endforeach +
    + + Hey Aria... +
    +
    +
    +
    + + {{-- Store Badges --}} +
    +
    +
    +
    + + + +
    +

    Download on the

    +

    App Store

    +
    +
    +
    + Bald +
    +
    +
    +
    + + + + + + +
    +

    Get it on

    +

    Google Play

    +
    +
    +
    + Bald +
    +
    + +
    + +
    + +
    +
    + + + {{-- ============================================================ + SECTION 7 β€” PRICING + ============================================================ --}} +
    +
    + +
    +

    Transparent und fair

    +

    Zwei PlΓ€ne. Keine versteckten Kosten.

    +
    + +
    + + {{-- FREE --}} +
    +
    +

    Kostenlos

    +
    + 0€ + / Monat +
    +
    +
    +
      + @foreach([ + [true, '500 Credits / Monat'], + [true, 'Kalender & Aufgaben'], + [true, 'Aria KI-Assistent'], + [true, 'iOS & Android App'], + [false, 'Push Notifications'], + [false, 'Automationen (Pro)'], + [false, 'E-Mail Erinnerungen'], + ] as [$on, $label]) +
    • + @if($on) + + @else + + @endif + {{ $label }} +
    • + @endforeach +
    + + Kostenlos starten + +
    + + {{-- PRO --}} +
    +
    + Beliebteste Wahl +
    +
    +

    Pro

    +
    + 12€ + / Monat +
    +
    +
    +
      + @foreach([ + '10.000 Credits / Monat', + 'Alles aus Free', + 'Push Notifications', + 'Alle Automationen', + 'E-Mail Erinnerungen', + 'Google Calendar Sync', + ] as $label) +
    • + + {{ $label }} +
    • + @endforeach +
    + + Pro testen + +
    + +
    + +

    + Beta-Nutzer erhalten 50% Rabatt auf den Pro Plan β€” fΓΌr immer. +

    + +
    +
    + + + {{-- ============================================================ + SECTION 8 β€” FAQ + ============================================================ --}} +
    +
    + +
    +

    HΓ€ufige Fragen

    +

    Alles was du wissen musst

    +
    + +
    + + @php + $faqs = [ + [ + 'q' => 'Was sind Credits?', + 'a' => 'Credits werden fΓΌr KI-Aktionen verwendet. Ein einfaches GesprΓ€ch kostet ~5 Credits, eine Aktion wie Termin erstellen ~30 Credits. Free Nutzer bekommen 500 Credits/Monat, Pro Nutzer 10.000.', + ], + [ + 'q' => 'Muss ich eine Kreditkarte angeben?', + 'a' => 'Nein. Du kannst Aziros kostenlos starten ohne Zahlungsdaten anzugeben. Erst beim Upgrade auf Pro wird eine Zahlungsmethode benΓΆtigt.', + ], + [ + 'q' => 'Wie sicher sind meine Daten?', + 'a' => 'Deine Daten werden ausschließlich auf EU-Servern gespeichert und verarbeitet. Aziros ist vollstΓ€ndig DSGVO-konform. Wir verkaufen keine Daten an Dritte.', + ], + [ + 'q' => 'Was ist der Unterschied zwischen Free und Pro?', + 'a' => 'Der Free Plan enthΓ€lt alle Grundfunktionen (Kalender, Aufgaben, Aria KI-Assistent) mit 500 Credits/Monat. Pro schaltet Automationen, Push Notifications, E-Mail Erinnerungen und Google Calendar Sync frei β€” mit 10.000 Credits/Monat.', + ], + [ + 'q' => 'Kann ich jederzeit kΓΌndigen?', + 'a' => 'Ja, jederzeit. Kein Abo, keine Mindestlaufzeit. Du kannst deinen Pro Plan in den Einstellungen monatlich kΓΌndigen β€” du verlierst keine Daten.', + ], + ]; + @endphp + + @foreach($faqs as $faq) +
    + +
    +

    {{ $faq['a'] }}

    +
    +
    + @endforeach + +
    + +
    +
    + + + {{-- ============================================================ + FOOTER + ============================================================ --}} + + +
    diff --git a/src/resources/views/livewire/homepage/impressum.blade.php b/src/resources/views/livewire/homepage/impressum.blade.php new file mode 100644 index 0000000..97e6b6b --- /dev/null +++ b/src/resources/views/livewire/homepage/impressum.blade.php @@ -0,0 +1,137 @@ +
    + + @include('partials.homepage.navbar') + + {{-- ============================================================ + HEADER + ============================================================ --}} +
    +
    + +
    +
    + + Pflichtangaben nach Β§ 5 ECG +
    +

    Impressum

    +
    +
    + + + {{-- ============================================================ + CONTENT + ============================================================ --}} +
    +
    +
    + + {{-- Unternehmensinfo --}} +
    +
    + +
    +

    Angaben gemÀß § 5 ECG

    +
    +

    aziros GmbH

    +

    Musterstraße 1

    +

    1010 Wien

    +

    Γ–sterreich

    +
    +
    + + {{-- Vertreten durch --}} +
    +
    + +
    +

    Vertreten durch

    +
    +

    Max Mustermann

    +

    GeschΓ€ftsfΓΌhrer

    +
    +
    + + {{-- Kontakt --}} +
    +
    + +
    +

    Kontakt

    +
    +

    Telefon: +43 (0) 1 234567

    +

    E-Mail: info@aziros.com

    +
    +
    + + {{-- Registereintrag --}} +
    +
    + +
    +

    Registereintrag

    +
    +

    Registergericht: Handelsgericht Wien

    +

    Firmenbuchnummer: FN 123456a

    +
    +
    + + {{-- USt-IdNr --}} +
    +
    + +
    +

    Umsatzsteuer-ID

    +
    +

    Umsatzsteuer-Identifikationsnummer gemÀß § 27a UStG:

    +

    ATU 12345678

    +
    +
    + + {{-- Streitschlichtung --}} +
    +
    + +
    +

    Streitschlichtung

    +
    +

    Die EuropΓ€ische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit. Wir sind nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.

    +
    +
    + +
    + + + {{-- Haftungsausschluss --}} +
    +

    Haftungsausschluss

    + +
    +
    +

    Haftung fΓΌr Inhalte

    +

    + Die Inhalte unserer Seiten wurden mit grâßter Sorgfalt erstellt. Für die Richtigkeit, VollstÀndigkeit und AktualitÀt der Inhalte kânnen wir jedoch keine GewÀhr übernehmen. Als Diensteanbieter sind wir gemÀß § 16 ECG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. +

    +
    + +
    +

    Haftung fΓΌr Links

    +

    + Unser Angebot enthΓ€lt Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. FΓΌr die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. +

    +
    + +
    +

    Urheberrecht

    +

    + Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem âsterreichischen Urheberrecht. Die VervielfÀltigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung der aziros GmbH. +

    +
    +
    +
    + +
    +
    + + @include('partials.homepage.footer') + +
    diff --git a/src/resources/views/livewire/homepage/index.blade.php b/src/resources/views/livewire/homepage/index.blade.php new file mode 100644 index 0000000..fb6d815 --- /dev/null +++ b/src/resources/views/livewire/homepage/index.blade.php @@ -0,0 +1,973 @@ +
    + + @include('partials.homepage.navbar') + + + {{-- ============================================================ + HERO + ============================================================ --}} +
    + + {{-- Background glows --}} +
    +
    +
    + +
    + +
    + + {{-- LEFT: Copy --}} +
    + + {{-- Badge --}} +
    + + KI-gestΓΌtzte ProduktivitΓ€t β€” jetzt verfΓΌgbar +
    + + {{-- Headline --}} +
    +

    + Dein Kalender.
    + Dein Assistent.
    + + Dein Vorteil. + +

    +

    + aziros verbindet Kalender, Aufgaben und einen KI-Assistenten in einer Plattform. + Sag was du brauchst β€” der Rest passiert automatisch. +

    +
    + + {{-- CTAs --}} + + + {{-- Social proof --}} +
    +
    + @foreach([ + 'https://i.pravatar.cc/40?img=1', + 'https://i.pravatar.cc/40?img=5', + 'https://i.pravatar.cc/40?img=8', + 'https://i.pravatar.cc/40?img=12', + ] as $avatar) + User + @endforeach +
    +
    + 500+ Teams nutzen aziros tΓ€glich +
    +
    + +
    + + {{-- RIGHT: App Mock --}} +
    + + {{-- Floating badges --}} +
    +
    + +
    +
    +

    Termin erstellt

    +

    Meeting mit Jana – 14:00

    +
    +
    + +
    +
    + +
    +
    +

    KI-Assistent

    +

    "Morgen 10 Uhr frei?" β€” Ja

    +
    +
    + + {{-- App UI Mock --}} +
    + + {{-- Window bar --}} +
    +
    +
    +
    +
    +
    + app.aziros.com/calendar +
    +
    +
    + +
    + + {{-- Sidebar mock --}} +
    + @foreach([ + ['icon' => 'home', 'active' => false], + ['icon' => 'calendar', 'active' => true], + ['icon' => 'check-circle', 'active' => false], + ['icon' => 'document-text', 'active' => false], + ['icon' => 'users', 'active' => false], + ] as $item) +
    + @if($item['icon'] === 'home') + + @elseif($item['icon'] === 'calendar') + + @elseif($item['icon'] === 'check-circle') + + @elseif($item['icon'] === 'document-text') + + @else + + @endif +
    + @endforeach +
    + + {{-- Calendar mock --}} +
    + + {{-- Week header --}} +
    + April 2026 +
    +
    +
    +
    +
    + + {{-- Day columns --}} +
    + @foreach(['Mo', 'Di', 'Mi', 'Do', 'Fr'] as $i => $day) +
    +
    {{ $day }}
    +
    + {{ 13 + $i }} +
    +
    + @endforeach +
    + + {{-- Events mock --}} +
    +
    +
    +

    Team-Stand

    +

    09:00–09:30

    +
    +
    +
    +
    +

    Arzt

    +

    10:00–11:00

    +
    +
    +

    Deep Work

    +

    14:00–16:00

    +
    +
    +
    +
    +
    +

    Design Review

    +

    11:00–12:00

    +
    +
    +
    +
    +

    Zahnarzt

    +

    09:30–10:30

    +
    +
    +
    + + {{-- AI input mock --}} +
    + + Morgen Meeting mit Jana um 14 Uhr... +
    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + + + {{-- ============================================================ + TRUSTED BY + STATS + ============================================================ --}} +
    +
    + + {{-- Logos --}} +

    + Vertraut von Teams aus allen Branchen +

    +
    + @foreach(['Consulting', 'Design Studio', 'Tech GmbH', 'Agentur AG', 'Startup Hub'] as $company) + {{ $company }} + @endforeach +
    + + {{-- Stats --}} +
    + @foreach([ + ['value' => '500+', 'label' => 'Aktive Teams', 'icon' => 'users'], + ['value' => '2.4h', 'label' => 'Ø Zeit gespart / Woche', 'icon' => 'clock'], + ['value' => '98%', 'label' => 'Kundenzufriedenheit', 'icon' => 'heart'], + ['value' => '< 60s', 'label' => 'Setup-Zeit', 'icon' => 'bolt'], + ] as $stat) +
    +

    {{ $stat['value'] }}

    +

    {{ $stat['label'] }}

    +
    + @endforeach +
    + +
    +
    + + + {{-- ============================================================ + FEATURES GRID + ============================================================ --}} +
    +
    + + {{-- Header --}} +
    +
    + + Alle Werkzeuge in einer Plattform +
    +

    + Alles was du brauchst.
    + Nichts was du nicht brauchst. +

    +

    + Von der Terminplanung bis zur KI-Automatisierung β€” aziros deckt deinen Arbeitstag komplett ab. +

    +
    + + {{-- Feature cards --}} +
    + + @php + $features = [ + [ + 'icon' => 'calendar', + 'bg' => 'bg-indigo-50', + 'ic' => 'text-indigo-600', + 'tag' => 'bg-indigo-100 text-indigo-700', + 'title' => 'Intelligente Terminplanung', + 'desc' => 'Erstelle Termine per Texteingabe. Die KI erkennt automatisch Konflikte und schlΓ€gt freie Zeitfenster vor.', + 'pill' => 'KI-Konflikterkennung', + ], + [ + 'icon' => 'check-circle', + 'bg' => 'bg-teal-50', + 'ic' => 'text-teal-600', + 'tag' => 'bg-teal-100 text-teal-700', + 'title' => 'Aufgaben & Notizen', + 'desc' => 'Halte Ideen, To-Dos und Notizen fest. Priorisiere, verknΓΌpfe und erledige β€” ohne Chaos.', + 'pill' => 'Alles verknΓΌpft', + ], + [ + 'icon' => 'bolt', + 'bg' => 'bg-purple-50', + 'ic' => 'text-purple-600', + 'tag' => 'bg-purple-100 text-purple-700', + 'title' => 'Einfach sagen, fertig', + 'desc' => 'Schreib oder sprich einfach was du brauchst. Der Assistent plant, erinnert und organisiert fΓΌr dich.', + 'pill' => 'KI-Assistent', + ], + [ + 'icon' => 'users', + 'bg' => 'bg-amber-50', + 'ic' => 'text-amber-600', + 'tag' => 'bg-amber-100 text-amber-700', + 'title' => 'Team & Kontakte', + 'desc' => 'Behalte dein Netzwerk im Blick. Teile Termine und koordiniere Meetings mit Teilnehmern.', + 'pill' => 'Kollaboration', + ], + [ + 'icon' => 'arrow-path', + 'bg' => 'bg-green-50', + 'ic' => 'text-green-600', + 'tag' => 'bg-green-100 text-green-700', + 'title' => 'Routinen automatisieren', + 'desc' => 'Erstelle Regeln fΓΌr wiederkehrende AblΓ€ufe. Was sich wiederholt, muss nicht manuell gemacht werden.', + 'pill' => 'No-Code Regeln', + ], + [ + 'icon' => 'puzzle-piece', + 'bg' => 'bg-rose-50', + 'ic' => 'text-rose-600', + 'tag' => 'bg-rose-100 text-rose-700', + 'title' => 'Google & Outlook Sync', + 'desc' => 'Verbinde deine bestehenden Kalender. Alles bleibt synchron β€” auf jedem GerΓ€t, jederzeit.', + 'pill' => 'Google & Outlook', + ], + ]; + @endphp + + @foreach($features as $f) +
    + +
    +
    + @if($f['icon'] === 'calendar') + @elseif($f['icon'] === 'check-circle') + @elseif($f['icon'] === 'bolt') + @elseif($f['icon'] === 'users') + @elseif($f['icon'] === 'arrow-path') + @else + @endif +
    + + {{ $f['pill'] }} + +
    + +

    {{ $f['title'] }}

    +

    {{ $f['desc'] }}

    + +
    + @endforeach + +
    + +
    +
    + + + {{-- ============================================================ + HOW IT WORKS + ============================================================ --}} +
    +
    + +
    +

    + In 3 Schritten bereit +

    +

    + Kein Onboarding-Stress. In Minuten produktiv. +

    +
    + +
    + + {{-- Connecting line --}} + + + @foreach([ + ['step' => '01', 'icon' => 'user-plus', 'title' => 'Account erstellen', 'desc' => 'Registriere dich in unter 60 Sekunden. Keine Kreditkarte nΓΆtig β€” direkt loslegen.', 'color' => 'indigo'], + ['step' => '02', 'icon' => 'link', 'title' => 'Kalender verbinden', 'desc' => 'Sync dein Google oder Outlook Konto. Bestehende Termine werden automatisch importiert.', 'color' => 'purple'], + ['step' => '03', 'icon' => 'bolt', 'title' => 'KI nutzen', 'desc' => 'Schreib einfach was du brauchst. "Morgen Meeting um 10 Uhr" β€” der Rest passiert automatisch.', 'color' => 'teal'], + ] as $step) +
    + + {{-- Step circle --}} +
    +
    + @if($step['icon'] === 'user-plus') + @elseif($step['icon'] === 'link') + @else + @endif +
    + {{ $step['step'][1] }} +
    +
    +
    + +
    +

    {{ $step['title'] }}

    +

    {{ $step['desc'] }}

    +
    + +
    + @endforeach + +
    + +
    +
    + + + {{-- ============================================================ + FEATURE SHOWCASE β€” KI Assistent + ============================================================ --}} +
    +
    +
    + + {{-- Left: Chat mock --}} +
    + +
    + + {{-- Header --}} +
    +
    + +
    +
    +

    aziros Assistent

    +
    +
    +

    Online

    +
    +
    +
    + + {{-- Chat messages --}} +
    + + {{-- User message --}} +
    +
    + Erstell mir morgen um 9 ein Meeting mit dem Design Team, eine Stunde. +
    +
    + + {{-- AI response --}} +
    +
    + +
    +
    +

    Erledigt!

    +

    Design Team Meeting
    + Mo, 14.04 · 09:00–10:00 Uhr

    +
    +
    + + {{-- User message 2 --}} +
    +
    + Wann hab ich als nΓ€chstes eine Stunde frei fΓΌr den Zahnarzt? +
    +
    + + {{-- AI response 2 --}} +
    +
    + +
    +
    +

    NΓ€chste freie Stunde:

    +

    Di, 15.04 · 11:00–12:00 Uhr

    + +
    +
    + +
    + + {{-- Input --}} +
    +
    + Nachricht eingeben... +
    +
    + +
    +
    + +
    + + {{-- Floating stat --}} +
    +
    + +
    +
    +

    2.4h

    +

    Ø Zeit gespart / Woche

    +
    +
    + +
    + + {{-- Right: Copy --}} +
    + +
    +
    + + KI-Assistent +
    +

    + Einfach sagen.
    + Der Rest passiert. +

    +

    + Kein aufwΓ€ndiges Klicken durch MenΓΌs. Schreib einfach was du brauchst β€” + der KI-Assistent erledigt den Rest sofort. +

    +
    + +
    + @foreach([ + ['icon' => 'calendar', 'title' => 'Termine per Text', 'desc' => '"Morgen 14 Uhr Zahnarzt fΓΌr 1 Stunde" β€” direkt im Kalender.'], + ['icon' => 'magnifying-glass', 'title' => 'Freie Slots finden', 'desc' => 'Frag wann du als nΓ€chstes Zeit hast β€” sofortige Antwort.'], + ['icon' => 'bell', 'title' => 'Erinnerungen automatisch', 'desc' => 'Wichtige Termine werden automatisch mit Reminder versehen.'], + ] as $p) +
    +
    + @if($p['icon'] === 'calendar') + @elseif($p['icon'] === 'magnifying-glass') + @else + @endif +
    +
    +

    {{ $p['title'] }}

    +

    {{ $p['desc'] }}

    +
    +
    + @endforeach +
    + +
    + +
    +
    +
    + + + {{-- ============================================================ + MOBILE APP + ============================================================ --}} +
    +
    + +
    + + {{-- LEFT: Text + App Store Badges --}} +
    + +
    +
    + + Mobile App +
    +

    + Aziros in
    + deiner Tasche +

    +

    + Verwalte Termine, Aufgaben und Kontakte β€” + und sprich einfach mit Aria, wo immer du bist. +

    +
    + + {{-- Feature points --}} +
    + @foreach([ + ['icon' => 'microphone', 'title' => '"Hey Aria"', 'desc' => 'Sprachsteuerung direkt aus der App'], + ['icon' => 'calendar', 'title' => 'Immer dabei', 'desc' => 'Termine, Aufgaben und Notizen immer dabei'], + ['icon' => 'bell', 'title' => 'Push-Erinnerungen', 'desc' => 'Push-Erinnerungen zur richtigen Zeit (Pro)'], + ] as $ap) +
    +
    + @if($ap['icon'] === 'microphone') + @elseif($ap['icon'] === 'calendar') + @else + @endif +
    +
    +

    {{ $ap['title'] }}

    +

    {{ $ap['desc'] }}

    +
    +
    + @endforeach +
    + + {{-- App Store Badges (Bald verfΓΌgbar) --}} +
    + + {{-- iOS --}} +
    +
    +
    + + + +
    +

    Download on the

    +

    App Store

    +
    +
    +
    + + Bald + +
    + + {{-- Android --}} +
    +
    +
    + + + + + + +
    +

    Get it on

    +

    Google Play

    +
    +
    +
    + + Bald + +
    + +
    + +
    + + {{-- RIGHT: iPhone Mockup --}} +
    +
    + + {{-- Phone frame --}} +
    + + {{-- Notch --}} +
    + + {{-- Screen --}} +
    + + {{-- Status bar --}} +
    + 9:41 +
    + + + + + +
    +
    + + {{-- App header --}} +
    +
    +
    +
    + +
    + aziros +
    +
    + +
    +
    +
    + + {{-- Content --}} +
    + +
    +

    Heute

    +

    Donnerstag, 17. April

    +
    + +
    +
    +

    Team-Standup

    +

    09:00 – 09:30

    +
    +
    +

    Design Review

    +

    11:00 – 12:00

    +
    +
    +

    Kundencall

    +

    14:00 – 14:30

    +
    +
    +

    Zahnarzt

    +

    16:00 – 17:00

    +
    +
    + + {{-- Aria voice input --}} +
    + + Hey Aria... +
    + @foreach([2, 3, 2, 4, 3] as $bh) +
    + @endforeach +
    +
    + +
    + +
    + +
    + + {{-- Floating: Push-Benachrichtigung --}} +
    +
    + +
    +
    +

    Erinnerung

    +

    Standup in 5 Min.

    +
    +
    + + {{-- Floating: Hey Aria --}} +
    + +

    Hey Aria

    +
    + @foreach([2, 3, 2] as $wh) +
    + @endforeach +
    +
    + +
    +
    + +
    +
    +
    + + + {{-- ============================================================ + PRICING TEASER + ============================================================ --}} +
    +
    + +
    + + {{-- Left: Copy --}} +
    +
    + + Einfaches Preismodell +
    +

    + Kostenlos starten.
    + Pro ab 13 € / Monat. +

    +

    + Zwei PlΓ€ne. Keine versteckten Kosten. Der Free-Plan ist dauerhaft kostenlos β€” upgrade erst, wenn du bereit bist. Jederzeit kΓΌndbar, ohne Mindestlaufzeit. +

    + +
    + + {{-- Right: Plan cards preview --}} +
    + + {{-- Free --}} +
    +

    Free

    +
    + 0 € + fΓΌr immer +
    +
    +
      + @foreach(['Kalender & Termine', 'Aufgaben & Notizen', 'KI-Assistent (Basis)', 'Kontaktverwaltung'] as $f) +
    • + + {{ $f }} +
    • + @endforeach +
    + + Kostenlos starten + +
    + + {{-- Pro --}} +
    +
    + Beliebt +
    +

    Pro

    +
    + 13 € + / Monat +
    +
    +
      + @foreach(['Alles aus Free', 'Google & Outlook Sync', 'Automatisierungen', 'PrioritΓ€ts-Support'] as $f) +
    • + + {{ $f }} +
    • + @endforeach +
    + + Pro starten + +
    + +
    + +
    + +
    +
    + + + {{-- ============================================================ + TESTIMONIALS + ============================================================ --}} +
    +
    + +
    +

    Was unsere Nutzer sagen

    +

    Echte Erfahrungen von echten Teams

    +
    + +
    + @foreach([ + [ + 'quote' => 'Seit ich aziros nutze, vergesse ich keine Termine mehr. Der KI-Assistent ist wie ein persΓΆnlicher Kalender-Manager.', + 'name' => 'Sarah M.', + 'role' => 'Freelance Designerin', + 'img' => 'https://i.pravatar.cc/80?img=25', + ], + [ + 'quote' => 'Unser Team koordiniert Termine jetzt 3x schneller. Die Konflikt-Erkennung hat uns schon mehrfach gerettet.', + 'name' => 'Thomas K.', + 'role' => 'Projektleiter, Tech-Startup', + 'img' => 'https://i.pravatar.cc/80?img=33', + ], + [ + 'quote' => 'Endlich alles an einem Ort. Kalender, Aufgaben und Notizen β€” ohne zwischen fΓΌnf Apps hin- und herzuwechseln.', + 'name' => 'Lisa H.', + 'role' => 'Operations Managerin', + 'img' => 'https://i.pravatar.cc/80?img=47', + ], + ] as $t) +
    + + {{-- Stars --}} +
    + @for($i = 0; $i < 5; $i++) + + + + @endfor +
    + +

    "{{ $t['quote'] }}"

    + +
    + {{ $t['name'] }} +
    +

    {{ $t['name'] }}

    +

    {{ $t['role'] }}

    +
    +
    + +
    + @endforeach +
    + +
    +
    + + + {{-- ============================================================ + CTA FINAL + ============================================================ --}} +
    + +
    +
    +
    +
    + +
    + +
    +

    + Dein Arbeitstag verdient
    ein Upgrade +

    +

    + Schließe dich 500+ Teams an, die mit aziros mehr schaffen β€” mit weniger Aufwand. +

    +
    + + + +
    + @foreach(['Kostenloser Plan verfΓΌgbar', 'Pro ab 13 €/Monat', 'Jederzeit kΓΌndbar'] as $t) + + + {{ $t }} + + @endforeach +
    + +
    +
    + + + @include('partials.homepage.footer') + +
    diff --git a/src/resources/views/livewire/homepage/kontakt.blade.php b/src/resources/views/livewire/homepage/kontakt.blade.php new file mode 100644 index 0000000..d3b6fc5 --- /dev/null +++ b/src/resources/views/livewire/homepage/kontakt.blade.php @@ -0,0 +1,217 @@ +
    + + @include('partials.homepage.navbar') + + {{-- ============================================================ + HERO + ============================================================ --}} +
    +
    +
    + +
    +
    + + Wir freuen uns auf dich +
    +

    + Schreib uns
    + eine Nachricht +

    +

    + Hast du Fragen, Feedback oder mΓΆchtest mehr ΓΌber aziros erfahren? Wir antworten in der Regel innerhalb von 24 Stunden. +

    +
    +
    + + + {{-- ============================================================ + CONTACT INFO CARDS + ============================================================ --}} +
    +
    +
    + @foreach([ + ['icon' => 'envelope', 'bg' => 'bg-indigo-50', 'ic' => 'text-indigo-600', 'border' => 'border-indigo-100', 'title' => 'E-Mail', 'desc' => 'info@aziros.com', 'link' => 'mailto:info@aziros.com', 'sub' => 'FΓΌr allgemeine Anfragen'], + ['icon' => 'phone', 'bg' => 'bg-teal-50', 'ic' => 'text-teal-600', 'border' => 'border-teal-100', 'title' => 'Telefon', 'desc' => '+43 (0) 1 234567', 'link' => 'tel:+4312345678', 'sub' => 'Mo–Fr, 09:00–18:00'], + ['icon' => 'map-pin', 'bg' => 'bg-purple-50', 'ic' => 'text-purple-600', 'border' => 'border-purple-100', 'title' => 'Adresse', 'desc' => 'Musterstraße 1, 1010 Wien', 'link' => null, 'sub' => 'Γ–sterreich'], + ['icon' => 'clock', 'bg' => 'bg-amber-50', 'ic' => 'text-amber-600', 'border' => 'border-amber-100', 'title' => 'Antwortzeit', 'desc' => 'Unter 24 Stunden', 'link' => null, 'sub' => 'In der Regel schneller'], + ] as $info) +
    +
    + @if($info['icon'] === 'envelope') + @elseif($info['icon'] === 'phone') + @elseif($info['icon'] === 'map-pin') + @else + @endif +
    +
    +

    {{ $info['title'] }}

    + @if($info['link']) + {{ $info['desc'] }} + @else +

    {{ $info['desc'] }}

    + @endif +

    {{ $info['sub'] }}

    +
    +
    + @endforeach +
    +
    +
    + + + {{-- ============================================================ + FORM + ============================================================ --}} +
    +
    + +
    + + {{-- LEFT: Form --}} +
    + + @if($sent) +
    +
    + +
    +

    Nachricht gesendet!

    +

    + Vielen Dank fΓΌr deine Nachricht. Wir melden uns so schnell wie mΓΆglich bei dir β€” in der Regel innerhalb von 24 Stunden. +

    + +
    + @else +
    +
    +

    Nachricht senden

    +

    FΓΌlle das Formular aus und wir melden uns bei dir.

    +
    + +
    + +
    + {{-- Name --}} +
    + + + @error('name')

    {{ $message }}

    @enderror +
    + + {{-- Email --}} +
    + + + @error('email')

    {{ $message }}

    @enderror +
    +
    + + {{-- Betreff --}} +
    + + + @error('betreff')

    {{ $message }}

    @enderror +
    + + {{-- Nachricht --}} +
    + + + @error('nachricht')

    {{ $message }}

    @enderror +
    + + {{-- Submit --}} + + +
    +
    + @endif + +
    + + {{-- RIGHT: Additional info --}} +
    + + {{-- FAQ teaser --}} +
    +
    +

    HΓ€ufige Fragen

    +

    Vielleicht findest du hier schon deine Antwort.

    +
    + +
    + @foreach([ + ['q' => 'Gibt es eine kostenlose Version?', 'a' => 'Ja! Der Free-Plan ist dauerhaft kostenlos β€” keine Kreditkarte nΓΆtig. Du kannst jederzeit upgraden.'], + ['q' => 'Wie kann ich mein Abo kΓΌndigen?', 'a' => 'Du kannst dein Abo jederzeit in den Einstellungen kΓΌndigen. Es gibt keine Mindestlaufzeit oder versteckte Kosten.'], + ['q' => 'Sind meine Daten sicher?', 'a' => 'Absolut. Wir hosten in der EU, sind DSGVO-konform und setzen keine Tracking-Cookies ein. Deine Daten gehΓΆren dir.'], + ] as $i => $faq) +
    + +
    +
    + {{ $faq['a'] }} +
    +
    +
    + @endforeach +
    + + + Alle PlΓ€ne ansehen + + +
    + + {{-- Trust --}} +
    +
    +

    Warum aziros vertrauen?

    +
    +
    + @foreach([ + 'DSGVO-konform β€” Hosting in der EU', + 'Keine Tracking-Cookies', + 'Made in Austria', + '500+ Teams vertrauen uns tΓ€glich', + 'Antwort innerhalb von 24h', + ] as $point) +
    +
    + +
    + {{ $point }} +
    + @endforeach +
    +
    + +
    + +
    + +
    +
    + + + @include('partials.homepage.footer') + +
    diff --git a/src/resources/views/livewire/homepage/preise.blade.php b/src/resources/views/livewire/homepage/preise.blade.php new file mode 100644 index 0000000..634cf2f --- /dev/null +++ b/src/resources/views/livewire/homepage/preise.blade.php @@ -0,0 +1,352 @@ +
    + + @include('partials.homepage.navbar') + + {{-- ============================================================ + HERO + ============================================================ --}} +
    +
    +
    +
    + +
    +
    + + Transparent & fair +
    +

    + Der richtige Plan
    + fΓΌr jedes Team +

    +

    + Zwei PlΓ€ne, keine versteckten Kosten. Starte kostenlos und upgrade wenn du bereit bist β€” jederzeit kΓΌndbar. +

    + + {{-- Billing Toggle --}} +
    +
    + + +
    +
    +
    +
    + + + {{-- ============================================================ + PLAN CARDS β€” 2 Spalten, zentriert, volle Breite + ============================================================ --}} +
    +
    + +
    + @foreach($plans as $plan) + + @endforeach +
    + +
    +
    + + + {{-- ============================================================ + TRUST BADGES + ============================================================ --}} +
    +
    +
    + @foreach([ + ['icon' => 'shield-check', 'bg' => 'bg-green-50', 'ic' => 'text-green-600', 'title' => 'DSGVO-konform', 'desc' => 'Hosting in der EU, deine Daten bleiben geschΓΌtzt'], + ['icon' => 'credit-card', 'bg' => 'bg-indigo-50', 'ic' => 'text-indigo-600', 'title' => 'Keine Kreditkarte nΓΆtig', 'desc' => 'Starte kostenlos, ohne Zahlungsdaten'], + ['icon' => 'arrow-path', 'bg' => 'bg-purple-50', 'ic' => 'text-purple-600', 'title' => 'Jederzeit wechseln', 'desc' => 'Upgrade, downgrade oder kΓΌndige β€” ganz ohne Stress'], + ['icon' => 'clock', 'bg' => 'bg-amber-50', 'ic' => 'text-amber-600', 'title' => 'In 60 Sekunden startklar', 'desc' => 'Registrieren und sofort loslegen'], + ] as $badge) +
    +
    + @if($badge['icon'] === 'shield-check') + @elseif($badge['icon'] === 'credit-card') + @elseif($badge['icon'] === 'arrow-path') + @else + @endif +
    +
    +

    {{ $badge['title'] }}

    +

    {{ $badge['desc'] }}

    +
    +
    + @endforeach +
    +
    +
    + + + {{-- ============================================================ + FEATURE COMPARISON TABLE + ============================================================ --}} +
    +
    + +
    +

    Was ist inklusive?

    +

    Ein detaillierter Vergleich aller Funktionen

    +
    + +
    + + + + + @foreach($plans as $plan) + + @endforeach + + + + @foreach([ + ['name' => 'Kalender & Terminverwaltung', 'free' => true, 'pro' => true], + ['name' => 'Aufgaben & Notizen', 'free' => true, 'pro' => true], + ['name' => 'Kontaktverwaltung', 'free' => true, 'pro' => true], + ['name' => 'KI-Assistent (Basis)', 'free' => true, 'pro' => true], + ['name' => 'KI-Assistent (Erweitert)', 'free' => false, 'pro' => true], + ['name' => 'Google Calendar Sync', 'free' => false, 'pro' => true], + ['name' => 'Outlook Sync', 'free' => false, 'pro' => true], + ['name' => 'Automatisierungen', 'free' => false, 'pro' => true], + ['name' => 'Unbegrenzte Credits', 'free' => false, 'pro' => true], + ['name' => 'PrioritΓ€ts-Support', 'free' => false, 'pro' => true], + ] as $i => $row) + + + + + + @endforeach + + + + + @foreach($plans as $plan) + + @endforeach + + +
    Funktion + {{ $plan->name }} +

    + {{ $plan->isFree() ? '0 €' : number_format($plan->getMonthlyPrice() / 100, 0) . ' € / Monat' }} +

    +
    {{ $row['name'] }} + @if($row['free']) +
    + +
    + @else +
    + +
    + @endif +
    + @if($row['pro']) +
    + +
    + @else +
    + +
    + @endif +
    + + {{ $plan->isFree() ? 'Kostenlos starten' : 'Pro starten' }} + +
    +
    + +
    +
    + + + {{-- ============================================================ + FAQ + ============================================================ --}} +
    +
    + +
    +
    + + HΓ€ufige Fragen +
    +

    Noch Fragen?

    +

    Alles was du ΓΌber unsere PlΓ€ne wissen musst

    +
    + +
    + @foreach([ + ['q' => 'Kann ich jederzeit upgraden oder downgraden?', 'a' => 'Ja, du kannst deinen Plan jederzeit wechseln. Beim Upgrade wird der Restbetrag deines aktuellen Plans anteilig verrechnet. Beim Downgrade gilt der neue Plan ab der nΓ€chsten Abrechnungsperiode.'], + ['q' => 'Gibt es eine Mindestlaufzeit?', 'a' => 'Nein. Du kannst monatlich kΓΌndigen β€” ohne Vertragsbindung oder versteckte Kosten. Bei jΓ€hrlicher Zahlung wird der Restbetrag bei KΓΌndigung nicht erstattet.'], + ['q' => 'Ist der kostenlose Plan wirklich kostenlos?', 'a' => 'Ja, der Free-Plan ist dauerhaft kostenlos. Keine Kreditkarte erforderlich. Du kannst jederzeit auf Pro upgraden, wenn du mehr Funktionen benΓΆtigst.'], + ['q' => 'Welche Zahlungsmethoden akzeptiert ihr?', 'a' => 'Wir akzeptieren alle gΓ€ngigen Kreditkarten (Visa, Mastercard, AMEX) sowie SEPA-Lastschrift fΓΌr Kunden im Euro-Raum.'], + ['q' => 'Kann ich aziros im Team nutzen?', 'a' => 'Ja! Jedes Teammitglied benΓΆtigt ein eigenes Konto mit einem passenden Plan. Über die Kontakte-Funktion kΓΆnnt ihr Termine koordinieren und Kalender teilen.'], + ['q' => 'Was passiert mit meinen Daten nach einer KΓΌndigung?', 'a' => 'Deine Daten bleiben nach KΓΌndigung noch 30 Tage gespeichert. In dieser Zeit kannst du jederzeit wieder einsteigen. Danach werden alle Daten unwiderruflich gelΓΆscht.'], + ] as $i => $faq) +
    + +
    +
    + {{ $faq['a'] }} +
    +
    +
    + @endforeach +
    + +
    +
    + + + {{-- ============================================================ + CTA + ============================================================ --}} +
    +
    +
    +
    +
    + +
    +
    +

    Noch unsicher?

    +

    Starte kostenlos und ΓΌberzeuge dich selbst. Kein Risiko.

    +
    + +
    +
    + + + @include('partials.homepage.footer') + +
    diff --git a/src/resources/views/livewire/homepage/ueber-uns.blade.php b/src/resources/views/livewire/homepage/ueber-uns.blade.php new file mode 100644 index 0000000..eb0527b --- /dev/null +++ b/src/resources/views/livewire/homepage/ueber-uns.blade.php @@ -0,0 +1,276 @@ +
    + + @include('partials.homepage.navbar') + + {{-- ============================================================ + HERO + ============================================================ --}} +
    +
    +
    +
    + +
    +
    + + Die Geschichte hinter aziros +
    +

    + Wir bauen die Zukunft
    + der ProduktivitΓ€t +

    +

    + aziros entstand aus der Überzeugung, dass Kalender, Aufgaben und KI zusammengehΓΆren β€” in einer einzigen, einfach zu bedienenden Plattform. Made in Austria. +

    +
    +
    + + + {{-- ============================================================ + STATS BAR + ============================================================ --}} +
    +
    +
    + @foreach([ + ['value' => '2024', 'label' => 'GegrΓΌndet', 'icon' => 'calendar'], + ['value' => '500+', 'label' => 'Aktive Teams', 'icon' => 'users'], + ['value' => '10+', 'label' => 'Teammitglieder', 'icon' => 'user-group'], + ['value' => 'Γ–sterreich', 'label' => 'Hauptsitz', 'icon' => 'map-pin'], + ] as $stat) +
    +
    + @if($stat['icon'] === 'calendar') + @elseif($stat['icon'] === 'users') + @elseif($stat['icon'] === 'user-group') + @else + @endif +
    +

    {{ $stat['value'] }}

    +

    {{ $stat['label'] }}

    +
    + @endforeach +
    +
    +
    + + + {{-- ============================================================ + MISSION + ============================================================ --}} +
    +
    +
    + + {{-- Left: Text --}} +
    +
    +
    + + Unsere Mission +
    +

    + ProduktivitΓ€t
    + neu gedacht +

    +
    +

    + Wir glauben, dass ProduktivitΓ€tstools zu kompliziert geworden sind. Teams jonglieren zwischen fΓΌnf verschiedenen Apps β€” Kalender hier, Aufgaben dort, Notizen woanders β€” und verlieren dabei wertvolle Zeit. +

    +

    + aziros vereint alles an einem Ort: Intelligente Terminplanung, Aufgabenverwaltung und einen KI-Assistenten, der versteht was du brauchst. Sag einfach was du willst β€” der Rest passiert automatisch. +

    + +
    + + {{-- Right: Values --}} +
    + @foreach([ + ['icon' => 'light-bulb', 'bg' => 'bg-amber-50', 'ic' => 'text-amber-600', 'border' => 'border-amber-100', 'title' => 'Einfachheit zuerst', 'desc' => 'Wir glauben, dass die besten Tools die sind, die man nicht erklΓ€ren muss. Jede Funktion wird so einfach wie mΓΆglich gebaut β€” keine unnΓΆtige KomplexitΓ€t, keine ΓΌberladenen MenΓΌs.'], + ['icon' => 'shield-check', 'bg' => 'bg-green-50', 'ic' => 'text-green-600', 'border' => 'border-green-100', 'title' => 'Datenschutz by Design', 'desc' => 'Deine Daten gehΓΆren dir. Wir hosten in der EU, folgen der DSGVO und setzen keine Tracking-Cookies ein. Transparenz und Vertrauen stehen bei uns an erster Stelle.'], + ['icon' => 'rocket-launch', 'bg' => 'bg-indigo-50', 'ic' => 'text-indigo-600', 'border' => 'border-indigo-100', 'title' => 'StΓ€ndige Innovation', 'desc' => 'Wir integrieren die neueste KI-Technologie, um dir immer einen Schritt voraus zu helfen. Neue Features werden regelmÀßig ausgerollt β€” basierend auf echtem Nutzerfeedback.'], + ] as $value) +
    +
    + @if($value['icon'] === 'light-bulb') + @elseif($value['icon'] === 'shield-check') + @else + @endif +
    +
    +

    {{ $value['title'] }}

    +

    {{ $value['desc'] }}

    +
    +
    + @endforeach +
    + +
    +
    +
    + + + {{-- ============================================================ + WHY AZIROS β€” Full-width feature showcase + ============================================================ --}} +
    +
    + +
    +

    Warum aziros?

    +

    Was uns von anderen ProduktivitΓ€tstools unterscheidet

    +
    + +
    + @foreach([ + ['icon' => 'bolt', 'bg' => 'bg-gradient-to-br from-indigo-50 to-indigo-100/50', 'ic' => 'text-indigo-600', 'title' => 'KI-first Ansatz', 'desc' => 'Der KI-Assistent ist keine ErgΓ€nzung β€” er ist das HerzstΓΌck. NatΓΌrliche Sprache statt komplizierter MenΓΌs. Sag was du brauchst, der Rest passiert automatisch.'], + ['icon' => 'globe-alt', 'bg' => 'bg-gradient-to-br from-green-50 to-green-100/50', 'ic' => 'text-green-600', 'title' => 'Made in Austria', 'desc' => 'Entwickelt in Γ–sterreich, gehostet in der EU. DSGVO-konform von Grund auf, mit Fokus auf Datenschutz, Sicherheit und Transparenz.'], + ['icon' => 'puzzle-piece', 'bg' => 'bg-gradient-to-br from-purple-50 to-purple-100/50', 'ic' => 'text-purple-600', 'title' => 'Alles in einem', 'desc' => 'Kalender, Aufgaben, Notizen, Kontakte und Automatisierung β€” ohne zwischen fΓΌnf Apps zu wechseln. Eine Plattform fΓΌr deinen gesamten Arbeitstag.'], + ] as $f) +
    +
    + @if($f['icon'] === 'bolt') + @elseif($f['icon'] === 'globe-alt') + @else + @endif +
    +

    {{ $f['title'] }}

    +

    {{ $f['desc'] }}

    +
    + @endforeach +
    + +
    +
    + + + {{-- ============================================================ + TEAM + ============================================================ --}} +
    +
    + +
    +
    + + Unser Team +
    +

    Die Menschen hinter aziros

    +

    Leidenschaft für großartige Software

    +
    + +
    + @foreach([ + ['name' => 'Max Mustermann', 'role' => 'CEO & GrΓΌnder', 'img' => 'https://i.pravatar.cc/300?img=11'], + ['name' => 'Anna Schmidt', 'role' => 'CTO', 'img' => 'https://i.pravatar.cc/300?img=25'], + ['name' => 'David Weber', 'role' => 'Lead Designer', 'img' => 'https://i.pravatar.cc/300?img=33'], + ['name' => 'Lena Fischer', 'role' => 'Head of Product', 'img' => 'https://i.pravatar.cc/300?img=47'], + ] as $member) +
    +
    +
    + {{ $member['name'] }} +
    +
    +

    {{ $member['name'] }}

    +

    {{ $member['role'] }}

    +
    +
    +
    + @endforeach +
    + +
    +
    + + + {{-- ============================================================ + TIMELINE + ============================================================ --}} +
    +
    + +
    +

    Unsere Reise

    +

    Von der Idee zur Plattform

    +
    + +
    + {{-- Vertical line --}} +
    + + @foreach([ + ['year' => '2024 Q1', 'title' => 'Die Idee', 'desc' => 'aziros entsteht aus der Frustration ΓΌber fragmentierte ProduktivitΓ€tstools. Die erste Vision: Kalender + KI in einer App.'], + ['year' => '2024 Q3', 'title' => 'Erste Beta', 'desc' => 'Die erste Version geht live β€” mit Kalender, Aufgaben und einem KI-Assistenten, der natΓΌrliche Sprache versteht.'], + ['year' => '2025 Q1', 'title' => 'Google & Outlook Sync', 'desc' => 'Integration mit Google Calendar und Microsoft Outlook. Bestehende Kalender werden nahtlos eingebunden.'], + ['year' => '2025 Q2', 'title' => 'Automatisierungen', 'desc' => 'No-Code Automatisierungsregeln machen wiederkehrende AblΓ€ufe zum Kinderspiel. Und das ist erst der Anfang.'], + ] as $i => $milestone) +
    + + {{-- Dot --}} +
    + + {{-- Content --}} +
    +
    + {{ $milestone['year'] }} +

    {{ $milestone['title'] }}

    +

    {{ $milestone['desc'] }}

    +
    +
    + +
    + @endforeach +
    + +
    +
    + + + {{-- ============================================================ + CTA + ============================================================ --}} +
    +
    +
    +
    +
    + +
    +
    +

    + Werde Teil der aziros Community +

    +

    + Starte noch heute kostenlos und erlebe ProduktivitΓ€t neu β€” ohne Risiko, ohne Kreditkarte. +

    +
    + +
    +
    + + + @include('partials.homepage.footer') + +
    diff --git a/src/resources/views/livewire/integration/index.blade.php b/src/resources/views/livewire/integration/index.blade.php new file mode 100644 index 0000000..1aaea8f --- /dev/null +++ b/src/resources/views/livewire/integration/index.blade.php @@ -0,0 +1,212 @@ +
    + + {{-- HEADER --}} +
    +

    {{ t('integrations.title') }}

    +

    {{ t('integrations.subtitle') }}

    +
    + + {{-- Flash: Verbindung erfolgreich --}} + @if($justConnected) +
    + + + @if($justConnected === 'google') {{ t('integrations.google') }} @else {{ t('integrations.outlook') }} @endif + {{ t('integrations.connected_msg') }} + +
    + @endif + + @if($connectionError) +
    + + {{ $connectionError }} +
    + @endif + + {{-- KALENDER-KARTEN --}} +
    +

    {{ t('integrations.calendar') }}

    + +
    + + {{-- GOOGLE --}} + @php $g = $google; @endphp +
    +
    + + {{-- Kopfzeile: Icon + Name + Badge --}} +
    +
    +
    + + + + + + +
    +

    {{ t('integrations.google') }}

    +
    + + @if($g) + + {{ t('integrations.connected') }} + + @else + + {{ t('integrations.disconnected') }} + + @endif +
    + + {{-- Details --}} + @if($g) +
    + + {{ $g->provider_email }} + Β· + + @if($g->sync_mode === 'read') {{ t('integrations.read_only') }} + @elseif($g->sync_mode === 'write') {{ t('integrations.write_only') }} + @else {{ t('integrations.bidirectional') }} + @endif + +
    + @else +

    {{ t('integrations.not_connected') }}

    + @endif + +
    + +
    +
    + + @if(!$canSync && !$g) + + + + {{ t('integrations.pro_required') }} + + + @endif +
    + + + {{-- OUTLOOK --}} + @php $o = $outlook; @endphp +
    +
    + + {{-- Kopfzeile: Icon + Name + Badge --}} +
    +
    +
    + + + + + + +
    +

    {{ t('integrations.outlook') }}

    +
    + + @if($o) + + {{ t('integrations.connected') }} + + @else + + {{ t('integrations.disconnected') }} + + @endif +
    + + {{-- Details --}} + @if($o) +
    + + {{ $o->provider_email }} + Β· + + @if($o->sync_mode === 'read') {{ t('integrations.read_only') }} + @elseif($o->sync_mode === 'write') {{ t('integrations.write_only') }} + @else {{ t('integrations.bidirectional') }} + @endif + +
    + @else +

    {{ t('integrations.not_connected') }}

    + @endif + +
    + +
    +
    + + @if(!$canSync && !$o) + + + + {{ t('integrations.pro_required') }} + + + @endif +
    + +
    +
    + + + {{-- ══════════════════════════════════════════════════════════════ --}} + {{-- INTEGRATION MODAL --}} + {{-- ══════════════════════════════════════════════════════════════ --}} +
    + + {{-- Backdrop --}} +
    + + {{-- Panel --}} +
    + + {{-- GOOGLE INHALT --}} + @if($activeProvider === 'google') + @include('livewire.integration.modals.provider', [ + 'integration' => $google, + 'provider' => 'google', + 'name' => t('integrations.google'), + 'connectRoute'=> route('integrations.google.redirect'), + 'canSync' => $canSync, + ]) + @elseif($activeProvider === 'outlook') + @include('livewire.integration.modals.provider', [ + 'integration' => $outlook, + 'provider' => 'outlook', + 'name' => t('integrations.outlook'), + 'connectRoute'=> route('integrations.outlook.redirect'), + 'canSync' => $canSync, + ]) + @endif + +
    +
    + +
    diff --git a/src/resources/views/livewire/integration/modals/form.blade.php b/src/resources/views/livewire/integration/modals/form.blade.php new file mode 100644 index 0000000..850d6a7 --- /dev/null +++ b/src/resources/views/livewire/integration/modals/form.blade.php @@ -0,0 +1,3 @@ +
    + {{-- Well begun is half done. - Aristotle --}} +
    diff --git a/src/resources/views/livewire/integration/modals/provider.blade.php b/src/resources/views/livewire/integration/modals/provider.blade.php new file mode 100644 index 0000000..84a759e --- /dev/null +++ b/src/resources/views/livewire/integration/modals/provider.blade.php @@ -0,0 +1,230 @@ +{{-- + Variables: + $integration – CalendarIntegration|null + $provider – 'google' | 'outlook' + $name – Display name + $connectRoute – OAuth redirect URL + $canSync – bool (plan has calendar_sync feature) +--}} + +{{-- HEADER --}} +
    +
    + @if($provider === 'google') +
    + + + + + + +
    + @else +
    + + + + + + +
    + @endif +
    +

    {{ $name }}

    + @if($integration) +

    {{ $integration->provider_email }}

    + @else +

    {{ t('integrations.not_connected') }}

    + @endif +
    +
    + + +
    + + +{{-- BODY --}} +
    + + @if($integration) + + {{-- STATUS --}} +
    + +
    +

    {{ t('integrations.connected') }}

    +

    {{ $integration->calendar_name }}

    +
    +
    + + {{-- TOKEN STATUS --}} + @if($integration->isExpired()) +
    + +
    +

    {{ t('integrations.token_expired') }}

    +

    {{ t('integrations.reconnect_hint') }}

    +
    +
    + @endif + + {{-- SYNC MODE --}} +
    +
    +

    {{ t('integrations.sync_mode') }}

    + @if(!$canSync) + + Pro + + @endif +
    + +
    + @foreach(['read' => t('integrations.read_only'), 'write' => t('integrations.write_only'), 'both' => t('integrations.bidirectional')] as $mode => $label) + + @endforeach +
    + + @if(!$canSync) +

    + + {{ t('integrations.sync_pro_hint') }} + Upgrade +

    + @endif +
    + + {{-- SYNC-STATUS & MANUELLER SYNC --}} + @if($provider === 'google' && in_array($integration->sync_mode, ['read', 'both'])) +
    +
    + + + @if($integration->last_synced_at) + {{ t('integrations.last_sync') }} {{ $integration->last_synced_at->diffForHumans() }} + @else + {{ t('integrations.not_synced') }} + @endif + +
    + +
    + + {{-- Watch-Status (Push-Benachrichtigungen) --}} +
    +
    + @if($integration->watch_channel_id && $integration->watch_expires_at?->isFuture()) + + {{ t('integrations.push_active') }} + Β· + {{ t('integrations.push_expires') }} {{ $integration->watch_expires_at->diffForHumans() }} + @else + + {{ t('integrations.push_inactive') }} + @endif +
    + @if(!$integration->watch_channel_id || $integration->watch_expires_at?->isPast()) + + @endif +
    + @else +
    + + {{ t('integrations.connected_since') }} {{ $integration->created_at->diffForHumans() }} +
    + @endif + + @else + + {{-- NICHT VERBUNDEN --}} +
    +
    + +
    +

    {{ t('integrations.not_connected') }}

    +

    + {{ t('integrations.connect_desc', ['name' => $name]) }} +

    +
    + + {{-- SYNC INFO wenn kein Pro --}} + @if(!$canSync) +
    +
    + +

    + {{ t('integrations.free_hint') }} {{ t('integrations.pro_plan') }} {{ t('integrations.required') }} +

    +
    +
    + @endif + + @endif + +
    + + +{{-- FOOTER --}} +
    + + @if($integration) + + @else +
    + @endif + +
    + + + @if($integration) + + {{ t('integrations.reconnect') }} + + @else + + + {{ t('common.connect') }} + + @endif +
    +
    diff --git a/src/resources/views/livewire/invoices/index.blade.php b/src/resources/views/livewire/invoices/index.blade.php new file mode 100644 index 0000000..90b3dae --- /dev/null +++ b/src/resources/views/livewire/invoices/index.blade.php @@ -0,0 +1,236 @@ +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('invoices.title') }}

    +

    {{ t('invoices.subtitle') }}

    +
    + @if($subscription) + + {{ t('invoices.manage_plan') }} + + @else + + {{ t('invoices.choose_plan') }} + + @endif +
    + + + {{-- ABRECHNUNGSÜBERSICHT --}} + @if($subscription) +
    + + {{-- Aktuelles Abo --}} +
    +

    {{ t('invoices.current_sub') }}

    +
    +
    +
    +

    {{ $subscription->plan_name }}

    + + {{ $subscription->status->label() }} + +
    +

    + {{ $subscription->interval === 'yearly' ? t('subscription.yearly') : t('subscription.monthly') }} + @if($subscription->starts_at) + Β· {{ t('common.since') }} {{ $subscription->starts_at->format('d.m.Y') }} + @endif +

    +
    +
    +

    + {{ number_format($subscription->price / 100, 2, ',', '.') }} € +

    +

    + / {{ $subscription->interval === 'yearly' ? t('common.year') : t('common.month') }} +

    +
    +
    +
    + + {{-- NΓ€chste Abrechnung --}} +
    +

    {{ t('invoices.next_billing') }}

    + + @if($subscription->ends_at && $daysLeft > 0) +
    +

    + {{ $daysLeft }} + {{ t('invoices.days') }} +

    +

    + am {{ $subscription->ends_at->format('d.m.Y') }} +

    +
    + @elseif($subscription->ends_at && $daysLeft <= 0) +
    +

    {{ t('invoices.due_now') }}

    +

    + {{ t('common.since') }} {{ $subscription->ends_at->format('d.m.Y') }} +

    +
    + @elseif($subscription->status === \App\Enums\SubscriptionStatus::Canceled && $subscription->ends_at) +
    +

    {{ t('invoices.ends_at') }}

    +

    + {{ $subscription->ends_at->format('d.m.Y') }} +

    +
    + @else +

    β€”

    + @endif +
    + +
    + @else +
    +
    + +
    +
    +

    {{ t('invoices.no_sub') }}

    +

    {{ t('invoices.no_sub_hint') }}

    +
    + + {{ t('invoices.view_plans') }} + +
    + @endif + + + {{-- OFFENE / FEHLGESCHLAGENE ZAHLUNGEN --}} + @if($openPayments->isNotEmpty()) +
    + @foreach($openPayments as $openPayment) +
    + +
    +
    + +
    +
    +

    + {{ $openPayment->status === \App\Enums\PaymentStatus::Failed + ? t('invoices.payment_failed') + : t('invoices.payment_pending') }} +

    +

    + {{ number_format($openPayment->amount / 100, 2, ',', '.') }} € + @if($openPayment->subscription?->plan_name) + Β· {{ $openPayment->subscription->plan_name }} + @endif + Β· {{ t('invoices.due_since') }} {{ $openPayment->created_at->format('d.m.Y') }} +

    +
    +
    + + +
    + @endforeach +
    + @endif + + + {{-- RECHNUNGSLISTE --}} +
    +

    {{ t('invoices.all') }}

    + + @if($payments->isEmpty()) +
    +
    + +
    +

    {{ t('invoices.no_invoices') }}

    +
    + @else + +
    + @foreach($payments as $payment) +
    + + {{-- STATUS ICON --}} +
    + @if($payment->status->icon() === 'check') + + @elseif($payment->status->icon() === 'x-mark') + + @elseif($payment->status->icon() === 'arrow-uturn-left') + + @else + + @endif +
    + + {{-- SHORTCUTS: MONAT + AKTION + INTERVAL --}} +
    + + {{ ($payment->paid_at ?? $payment->created_at)->locale('de')->isoFormat('MMM YYYY') }} + + @php + $reason = $payment->billing_reason ?? ''; + $badgeBg = str_starts_with($reason, 'upgrade:') ? 'bg-green-50 text-green-700' + : (str_starts_with($reason, 'downgrade:') ? 'bg-amber-50 text-amber-700' + : (str_starts_with($reason, 'refund:') ? 'bg-blue-50 text-blue-600' + : ($reason === 'subscription_create' ? 'bg-indigo-50 text-indigo-600' + : 'bg-gray-100 text-gray-600'))); + @endphp + + {{ $payment->actionLabel() }} + + @if($payment->subscription?->interval) + + @endif +
    + + {{-- STATUS BADGE + BETRAG --}} + @php $isRefund = str_starts_with($payment->billing_reason ?? '', 'refund:'); @endphp +
    + +

    + {{ $isRefund ? 'βˆ’' : '' }}{{ number_format($payment->amount / 100, 2, ',', '.') }} € +

    +
    + +
    + @endforeach +
    + + {{-- PAGINATION --}} + @if($payments->hasPages()) +
    + {{ $payments->firstItem() }}–{{ $payments->lastItem() }} {{ t('common.of') }} {{ $payments->total() }} +
    + @if($payments->onFirstPage()) + β€Ή + @else + + @endif + @if($payments->hasMorePages()) + + @else + β€Ί + @endif +
    +
    + @endif + + @endif +
    + +
    diff --git a/src/resources/views/livewire/notes/index.blade.php b/src/resources/views/livewire/notes/index.blade.php new file mode 100644 index 0000000..2582423 --- /dev/null +++ b/src/resources/views/livewire/notes/index.blade.php @@ -0,0 +1,195 @@ +@php + $colorOptions = [ + 'yellow' => ['bg' => 'bg-amber-400', 'label' => t('notes.color.yellow')], + 'blue' => ['bg' => 'bg-blue-400', 'label' => t('notes.color.blue')], + 'green' => ['bg' => 'bg-green-400', 'label' => t('notes.color.green')], + 'pink' => ['bg' => 'bg-pink-400', 'label' => t('notes.color.pink')], + 'purple' => ['bg' => 'bg-purple-400', 'label' => t('notes.color.purple')], + 'gray' => ['bg' => 'bg-gray-400', 'label' => t('notes.color.gray')], + ]; +@endphp + +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('notes.title') }}

    +

    {{ t('notes.subtitle') }}

    +
    + +
    + + + {{-- STATS --}} + @if($stats['total'] > 0) +
    +
    +
    + +
    +
    +

    {{ $stats['total'] }}

    +

    {{ t('notes.total') }}

    +
    +
    +
    +
    + +
    +
    +

    {{ $stats['pinned'] }}

    +

    {{ t('notes.pinned') }}

    +
    +
    +
    + @endif + + + {{-- SUCHE + FARBFILTER --}} +
    +
    + + +
    + + {{-- Farbfilter --}} +
    + + @foreach($colorOptions as $key => $c) + + @endforeach +
    +
    + + + {{-- FORMULAR (Erstellen / Bearbeiten) --}} + @if($showForm) +
    + +
    +

    {{ $editId ? t('notes.edit') : t('notes.new') }}

    + +
    + + + + + + @error('content')

    {{ $message }}

    @enderror + + {{-- Farbwahl --}} +
    + {{ t('notes.color_label') }} +
    + @foreach($colorOptions as $key => $c) + + @endforeach +
    +
    + +
    + + +
    + +
    + @endif + + + {{-- NOTIZEN GRID --}} + @if($notes->isEmpty()) + +
    +
    + +
    +

    {{ t('notes.no_notes') }}

    + @if($search || $filterColor) + + @else + + @endif +
    + + @else + +
    + @foreach($notes as $note) + @php $c = $note->colorClasses(); @endphp + +
    + + {{-- Pin-Badge --}} + @if($note->pinned) +
    + +
    + @endif + + {{-- Aktionen --}} +
    + + + +
    + + {{-- Inhalt --}} + @if($note->title) +

    {{ $note->title }}

    + @endif + +

    {{ $note->content }}

    + +

    + {{ $note->created_at->diffForHumans() }} +

    + +
    + @endforeach +
    + + @endif + +
    diff --git a/src/resources/views/livewire/notifications/bell.blade.php b/src/resources/views/livewire/notifications/bell.blade.php new file mode 100644 index 0000000..f911668 --- /dev/null +++ b/src/resources/views/livewire/notifications/bell.blade.php @@ -0,0 +1,182 @@ +
    + + {{-- Bell Button --}} + + + {{-- Dropdown --}} +
    + + {{-- Header --}} +
    + {{ t('notifications.title') }} + +
    + @if($unreadCount > 0) + + @endif + + @if(count($notifications) > 0) +
    + +
    + + +
    +
    + @endif +
    +
    + + {{-- Liste --}} +
    + @forelse($notifications as $n) +
    + + {{-- Klickbarer Bereich --}} + + + {{-- LΓΆschen-Button (erscheint bei Hover) --}} + +
    + @empty +
    + +

    {{ t('notifications.empty') }}

    +
    + @endforelse +
    + +
    +
    + +@script + +@endscript diff --git a/src/resources/views/livewire/payments/index.blade.php b/src/resources/views/livewire/payments/index.blade.php new file mode 100644 index 0000000..649c3d6 --- /dev/null +++ b/src/resources/views/livewire/payments/index.blade.php @@ -0,0 +1,3 @@ +
    + {{-- People find pleasure in different ways. I find it in keeping my mind clear. - Marcus Aurelius --}} +
    diff --git a/src/resources/views/livewire/plans/index.blade.php b/src/resources/views/livewire/plans/index.blade.php new file mode 100644 index 0000000..4b85a05 --- /dev/null +++ b/src/resources/views/livewire/plans/index.blade.php @@ -0,0 +1,289 @@ +
    + + {{-- HEADER --}} +
    +

    + {{ t('plans.choose') }} +

    +

    + {{ t('plans.subtitle') }} +

    +
    + +{{-- @if($subscription)--}} + +{{--
    --}} + +{{--
    --}} + +{{--
    --}} +{{--
    --}} +{{-- Aktiver Plan: {{ $subscription->plan?->name }}--}} +{{--
    --}} + +{{--
    --}} +{{-- {{ $subscription->isActive() ? 'Aktiv' : 'Inaktiv' }}--}} +{{-- @if($subscription->ends_at)--}} +{{-- β€’ endet am {{ $subscription->ends_at->format('d.m.Y') }}--}} +{{-- @endif--}} +{{--
    --}} +{{--
    --}} + +{{--
    --}} +{{-- {{ ucfirst($subscription->interval) }}--}} +{{--
    --}} + +{{--
    --}} + +{{--
    --}} + +{{-- @endif--}} + +
    + +
    + + + + + +
    + +
    + + {{-- VALUE SECTION --}} +
    + +
    +

    {{ t('plans.feature.all_in_one') }}

    +

    + {{ t('plans.feature.all_in_one_desc') }} +

    +
    + +
    +

    {{ t('plans.feature.smart_input') }}

    +

    + {{ t('plans.feature.smart_input_desc') }} +

    +
    + +
    +

    {{ t('plans.feature.ready') }}

    +

    + {{ t('plans.feature.ready_desc') }} +

    +
    + +
    + + {{-- CURRENT PLAN --}} + @if($currentPlanKey) +
    + {{ t('plans.current_plan') }} + {{ strtoupper(str_replace('_', ' ', $currentPlanKey)) }} +
    + @endif + + {{-- PLANS --}} + @php + $featuredPlan = $plans->firstWhere('is_featured', true); + $otherPlans = $plans->filter(fn($p) => !$p->is_featured)->values(); + $half = (int) floor($otherPlans->count() / 2); + $orderedPlans = $featuredPlan + ? $otherPlans->take($half)->push($featuredPlan)->concat($otherPlans->slice($half)) + : $plans; + @endphp +
    + + @foreach($orderedPlans as $plan) + + @php + $isCurrent = $currentPlanKey === $plan->plan_key; + $planFeatureKeys = $plan->features->pluck('key')->toArray(); + @endphp + + + + @endforeach + +
    + + +
    + + {{-- TRUST --}} +
    + +

    {{ t('plans.why') }}

    + +
    + +
    + βœ” + {{ t('plans.cancel_anytime') }} +
    + +
    + βœ” + {{ t('plans.no_hidden_costs') }} +
    + +
    + βœ” + {{ t('plans.up_down_anytime') }} +
    + +
    + +
    + + + {{-- FAQ --}} +
    + +

    {{ t('plans.faq') }}

    + +
    + +
    +

    + {{ t('plans.faq.switch') }} +

    +

    + {{ t('plans.faq.switch_a') }} +

    +
    + +
    +

    + {{ t('plans.faq.expire') }} +

    +

    + {{ t('plans.faq.expire_a') }} +

    +
    + +
    + +
    + +
    +
    diff --git a/src/resources/views/livewire/settings/index.blade.php b/src/resources/views/livewire/settings/index.blade.php new file mode 100644 index 0000000..05a7ffc --- /dev/null +++ b/src/resources/views/livewire/settings/index.blade.php @@ -0,0 +1,695 @@ +@php + $inputClass = 'w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-800 focus:outline-none focus:border-indigo-400 placeholder:text-gray-400 bg-white'; + $labelClass = 'block text-xs font-medium text-gray-500 mb-1.5'; +@endphp + +
    + + {{-- HEADER --}} +
    +

    {{ t('settings.title') }}

    +

    {{ t('settings.subtitle') }}

    +
    + + {{-- TAB NAVIGATION --}} +
    + @foreach([ + 'profile' => ['label' => t('settings.tab.profile'), 'icon' => 'user'], + 'security' => ['label' => t('settings.tab.security'), 'icon' => 'lock-closed'], + 'notifications' => ['label' => t('settings.tab.notifications'), 'icon' => 'bell'], + 'smtp' => ['label' => t('settings.tab.smtp'), 'icon' => 'envelope'], + 'credits' => ['label' => t('settings.tab.credits'), 'icon' => 'bolt'], + ...(!auth()->user()->isInternalUser() ? ['affiliate' => ['label' => 'Affiliate', 'icon' => 'gift']] : []), + 'account' => ['label' => t('settings.tab.account'), 'icon' => 'shield-exclamation'], + ] as $key => $tab) + + @endforeach +
    + + + {{-- ════════════════════════════════════════════════════════════ --}} + {{-- TAB: PROFIL --}} + {{-- ════════════════════════════════════════════════════════════ --}} +
    +
    + + {{-- Card Header --}} +
    +
    + +
    +
    +

    {{ t('settings.profile.title') }}

    +

    {{ t('settings.profile.subtitle') }}

    +
    +
    + + {{-- Card Body --}} +
    + +
    + + + @error('name')

    {{ $message }}

    @enderror +
    + +
    + + + @error('email')

    {{ $message }}

    @enderror +
    + +
    + + +
    + +
    + + +
    + +
    + + {{-- Card Footer --}} +
    + +
    + +
    +
    + + + {{-- ════════════════════════════════════════════════════════════ --}} + {{-- TAB: SICHERHEIT --}} + {{-- ════════════════════════════════════════════════════════════ --}} +
    +
    + +
    +
    + +
    +
    +

    {{ t('settings.security.title') }}

    +

    {{ t('settings.security.subtitle') }}

    +
    +
    + +
    + +
    + + + @error('current_password')

    {{ $message }}

    @enderror +
    + +
    + + + @error('new_password')

    {{ $message }}

    @enderror +
    + +
    + + +
    + +
    + +
    + +
    + +
    +
    + + + {{-- ════════════════════════════════════════════════════════════ --}} + {{-- TAB: BENACHRICHTIGUNGEN --}} + {{-- ════════════════════════════════════════════════════════════ --}} +
    +
    + +
    +
    + +
    +
    +

    Benachrichtigungen

    +

    Steuere wie und wann du benachrichtigt wirst

    +
    +
    + +
    + + {{-- In-App (immer aktiv) --}} +
    +
    +

    In-App Benachrichtigungen

    +

    System-Updates, Credit-Γ„nderungen und Aria-Aktionen

    +

    Immer aktiv β€” nicht deaktivierbar

    +
    +
    +
    +
    +
    +
    +
    + + {{-- Push (Pro) --}} + @php $isPro = auth()->user()->subscription?->isActive() ?? false; @endphp +
    +
    +
    +

    Push Benachrichtigungen

    + @if(!$isPro) + Pro + @endif +
    +

    Direkt auf dein GerΓ€t β€” auch wenn die App geschlossen ist

    + @if($isPro) +

    FΓΌr: Termin-Erinnerungen, Geburtstage, TΓ€gliche Agenda

    + @endif +
    +
    + @if($isPro) + + @else + + Upgraden β†’ + + @endif +
    +
    + + {{-- Email (Pro) --}} +
    +
    +
    +

    E-Mail Benachrichtigungen

    + @if(!$isPro) + Pro + @endif +
    +

    TΓ€gliche Agenda, Wochenvorschau und TagesrΓΌckblick per E-Mail

    + @if($isPro) +

    Wird gesendet von reminder@aziros.com

    + @endif +
    +
    + @if($isPro) + + @else + + Upgraden β†’ + + @endif +
    +
    + +
    + +
    +
    + + + {{-- ════════════════════════════════════════════════════════════ --}} + {{-- TAB: SMTP --}} + {{-- ════════════════════════════════════════════════════════════ --}} +
    +
    + +
    +
    + +
    +
    +

    {{ t('settings.smtp.title') }}

    +

    {{ t('settings.smtp.subtitle') }}

    +
    +
    + + {{-- Info Box --}} +
    +

    {{ t('settings.smtp.info_title') }}

    +

    {{ t('settings.smtp.info_body') }}

    +

    {{ t('settings.smtp.info_note') }}

    +
    + + {{-- Rate Limit Info --}} +
    +
    +
    +

    {{ t('settings.smtp.rate_title') }}

    +

    {{ t('settings.smtp.rate_desc') }}

    +
    +
    +

    {{ $emailsSentToday }}

    +

    {{ t('settings.smtp.today_sent') }}

    +
    +
    +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + @foreach(['tls' => 'TLS', 'ssl' => 'SSL', 'null' => t('settings.smtp.none')] as $val => $lbl) + + @endforeach +
    +
    + +
    + +
    + +
    +
    +

    {{ t('settings.smtp.test_title') }}

    +

    {{ t('settings.smtp.test_desc', ['email' => auth()->user()->email]) }}

    +
    +
    + + +
    +
    + + @if($smtpStatus === 'success') +
    + + {{ t('settings.smtp.test_success') }} +
    + @elseif($smtpStatus === 'error') +
    + + {{ t('settings.smtp.test_error', ['error' => $smtpError]) }} +
    + @endif + +
    + +
    +
    + + + {{-- ════════════════════════════════════════════════════════════ --}} + {{-- ════════════════════════════════════════════════════════════ --}} + {{-- TAB: CREDITS --}} + {{-- ════════════════════════════════════════════════════════════ --}} +
    + + {{-- Übersicht-Cards --}} +
    +
    +

    {{ t('settings.credits.plan_limit') }}

    +

    {{ $planLimit === 0 ? '∞' : number_format($planLimit) }}

    +

    {{ t('settings.credits.resets') }}

    +
    +
    +

    {{ t('settings.credits.balance') }}

    +

    {{ number_format($bonusLeft) }}

    +

    {{ t('settings.credits.no_expire') }}

    +
    +
    +

    {{ t('settings.credits.effective') }}

    +

    {{ $effLimit === 0 ? '∞' : number_format($effLimit) }}

    +

    {{ t('settings.credits.plan_plus') }}

    +
    +
    + + {{-- Progressbar --}} + @if($effLimit > 0) +
    +
    + {{ t('settings.credits.usage') }} + {{ number_format($monthUsage) }} / {{ number_format($effLimit) }} +
    + @php + $pct = min(100, $effLimit > 0 ? round($monthUsage / $effLimit * 100) : 0); + $barColor = match(true) { + $pct >= 100 => 'bg-red-500', + $pct >= 80 => 'bg-amber-400', + $pct >= 60 => 'bg-orange-400', + default => 'bg-indigo-500', + }; + @endphp +
    +
    +
    + @if($bonusLeft > 0 && $planLimit > 0) + @php $planPct = min(100, round($planLimit / $effLimit * 100)); @endphp +
    +
    + ↑ Plan ({{ number_format($planLimit) }}) +
    +
    +

    + {{ t('settings.credits.balance_note', ['limit' => number_format($planLimit)]) }} +

    + @endif +
    + @endif + + {{-- Transaktions-Verlauf --}} +
    +
    +

    {{ t('settings.credits.history') }}

    +

    {{ t('settings.credits.last_20') }}

    +
    + + @if($creditTransactions->isEmpty()) +
    +
    + +
    +

    {{ t('settings.credits.no_transactions') }}

    +
    + @else +
    + @foreach($creditTransactions as $tx) + @php + [$txLabel, $txColor, $txBg] = match($tx->type) { + 'onboarding' => [t('settings.credits.type.welcome'), '#059669', '#ECFDF5'], + 'affiliate' => [t('settings.credits.type.affiliate'), '#4F46E5', '#EEF2FF'], + 'admin_gift' => [t('settings.credits.type.gift'), '#9333EA', '#FAF5FF'], + 'refund' => [t('settings.credits.type.refund'), '#059669', '#ECFDF5'], + 'subscription' => [t('settings.credits.type.sub_bonus'), '#2563EB', '#EFF6FF'], + 'usage' => [t('settings.credits.type.usage'), '#DC2626', '#FEF2F2'], + 'event', 'event_update' => [t('settings.credits.type.event'), '#4F46E5', '#EEF2FF'], + 'task', 'task_update' => [t('settings.credits.type.task'), '#065F46', '#ECFDF5'], + 'note', 'note_update' => [t('settings.credits.type.note'), '#B45309', '#FFFBEB'], + 'contact' => [t('settings.credits.type.contact'), '#1E40AF', '#EFF6FF'], + 'email' => [t('settings.credits.type.email'), '#6B21A8', '#FAF5FF'], + 'multi' => [t('settings.credits.type.multi'), '#9D174D', '#FDF2F8'], + 'chat' => [t('settings.credits.type.chat'), '#374151', '#F3F4F6'], + default => [$tx->type, '#374151', '#F3F4F6'], + }; + @endphp +
    +
    +
    + + + {{ $txLabel }} + + {{ $tx->label }} +
    +
    +

    {{ $tx->created_at->diffForHumans() }}

    + @if(!empty($tx->duration_ms)) + Β· +

    {{ $tx->duration_ms < 1000 ? $tx->duration_ms . 'ms' : number_format($tx->duration_ms / 1000, 1) . 's' }}

    + @endif +
    +
    + + {{ $tx->amount > 0 ? '+' : '' }}{{ number_format($tx->amount) }} + +
    + @endforeach +
    + @endif +
    + +
    + + {{-- TAB: AFFILIATE --}} + {{-- ════════════════════════════════════════════════════════════ --}} + @if(!auth()->user()->isInternalUser()) +
    + +
    + + {{-- Card Header --}} +
    +
    + +
    +
    +

    {{ t('settings.affiliate.title') }}

    +

    {{ t('settings.affiliate.subtitle') }}

    +
    +
    + +
    + + @if($affiliate) + + {{-- Stats --}} +
    +
    +

    {{ $affiliate->total_referrals }}

    +

    {{ t('settings.affiliate.invited') }}

    +
    +
    +

    {{ $affiliate->qualified_referrals }}

    +

    {{ t('settings.affiliate.qualified') }}

    +
    +
    +

    {{ $affiliate->total_credits_earned }}

    +

    {{ t('settings.affiliate.earned') }}

    +
    +
    + + {{-- Referral Link --}} +
    + +
    + + +
    +
    +

    + {{ t('settings.affiliate.code') }} {{ $affiliate->code }} + Β· {{ t('settings.affiliate.reward') }} 500 Credits +

    +
    +
    + + {{-- Referrals Tabelle --}} + @if($referrals->isNotEmpty()) +
    + +
    + + + + + + + + + + + + @foreach($referrals as $referral) + @php + $badge = match($referral->status) { + 'pending' => [t('settings.affiliate.status.pending'), 'bg-amber-50 text-amber-700'], + 'qualified' => [t('settings.affiliate.status.qualified'), 'bg-green-50 text-green-700'], + 'paid' => [t('settings.affiliate.status.credited'), 'bg-indigo-50 text-indigo-700'], + 'cancelled' => [t('settings.affiliate.status.cancelled'), 'bg-red-50 text-red-700'], + default => [$referral->status, 'bg-gray-100 text-gray-600'], + }; + @endphp + + + + + + + + @endforeach + +
    {{ t('common.name') }}{{ t('settings.affiliate.registered') }}{{ t('settings.affiliate.qualified_at') }}{{ t('common.status') }}{{ t('common.credits') }}
    {{ $referral->referredUser?->name ?? t('settings.affiliate.status.unknown') }}{{ $referral->registered_at->format('d.m.Y') }} + {{ $referral->qualifies_at->format('d.m.Y') }} + @if($referral->status === 'pending') + ({{ $referral->qualifies_at->diffForHumans() }}) + @endif + + {{ $badge[0] }} + + {{ $referral->credits_awarded > 0 ? '+' . $referral->credits_awarded : 'β€”' }} +
    +
    +
    + @endif + + @else +
    + +

    {{ t('settings.affiliate.not_member') }}

    +

    {!! t('settings.affiliate.join_cta', ['credits' => '500 Credits']) !!}

    + +
    + @endif + +
    +
    +
    + @endif + + + {{-- ════════════════════════════════════════════════════════════ --}} + {{-- TAB: GEFAHRENZONE --}} + {{-- ════════════════════════════════════════════════════════════ --}} +
    +
    + +
    +
    + +
    +
    +

    {{ t('settings.account.title') }}

    +

    {{ t('settings.account.subtitle') }}

    +
    +
    + +
    +
    +
    +

    {{ t('settings.account.delete') }}

    +

    + {{ t('settings.account.delete_desc') }} +

    +
    + +
    +
    + +
    +
    + +
    diff --git a/src/resources/views/livewire/settings/modals/delete-account.blade.php b/src/resources/views/livewire/settings/modals/delete-account.blade.php new file mode 100644 index 0000000..449df67 --- /dev/null +++ b/src/resources/views/livewire/settings/modals/delete-account.blade.php @@ -0,0 +1,56 @@ +
    + + {{-- HEADER --}} +
    +
    + +
    +
    +

    + {{ t('settings.danger.confirm_title') }} +

    +

    + {{ t('settings.danger.confirm_description') }} +

    +
    +
    + + {{-- WARNING --}} +
    + {{ t('settings.danger.delete_description') }} +
    + + {{-- PASSWORD --}} +
    + + + @error('password') +

    {{ $message }}

    + @enderror +
    + + {{-- ACTIONS --}} +
    + + +
    + +
    diff --git a/src/resources/views/livewire/settings/modals/smtp-error.blade.php b/src/resources/views/livewire/settings/modals/smtp-error.blade.php new file mode 100644 index 0000000..ad36a39 --- /dev/null +++ b/src/resources/views/livewire/settings/modals/smtp-error.blade.php @@ -0,0 +1,46 @@ +
    + + {{-- HEADER --}} +
    +
    + +
    +
    +

    + {{ t('settings.smtp.error_title') }} +

    +

    + {{ t('settings.smtp.error_description') }} +

    +
    +
    + + {{-- ERROR BOX --}} +
    +
    + + {{ t('settings.smtp.error_details') }} + + +
    +
    {{ $message }}
    +
    + + {{-- ACTIONS --}} +
    + + +
    + +
    diff --git a/src/resources/views/livewire/subscription/index.blade.php b/src/resources/views/livewire/subscription/index.blade.php new file mode 100644 index 0000000..23194d6 --- /dev/null +++ b/src/resources/views/livewire/subscription/index.blade.php @@ -0,0 +1,292 @@ +
    + + {{-- HEADER --}} +
    +

    {{ t('subscription.title') }}

    +

    {{ t('subscription.subtitle') }}

    +
    + + + {{-- AKTUELLER PLAN --}} + @if($subscription) +
    + +
    +
    +

    {{ t('subscription.current_plan') }}

    +
    +

    {{ $subscription->plan_name }}

    + + {{ $subscription->status->label() }} + +
    +
    +
    +

    + {{ number_format($subscription->price / 100, 2, ',', '.') }} € +

    +

    + / {{ $subscription->interval === 'yearly' ? t('common.year') : t('common.month') }} +

    +
    +
    + +
    +
    +

    {{ t('subscription.duration') }}

    +

    + {{ $subscription->interval === 'yearly' ? t('subscription.yearly') : t('subscription.monthly') }} +

    +
    + @if($subscription->starts_at) +
    +

    {{ t('subscription.active_since') }}

    +

    {{ $subscription->starts_at->format('d.m.Y') }}

    +
    + @endif + @if($subscription->ends_at) +
    +

    + {{ $subscription->status === \App\Enums\SubscriptionStatus::Canceled ? t('subscription.ends_at') : t('subscription.renews_at') }} +

    +

    + {{ $subscription->ends_at->format('d.m.Y') }} +

    +
    + @endif + @if($subscription->plan?->credit_limit) +
    +

    {{ t('subscription.credits_month') }}

    +

    {{ number_format($subscription->plan->credit_limit) }}

    +
    + @endif +
    + +
    + @else +
    +
    + +
    +

    {{ t('subscription.no_plan') }}

    +

    {{ t('subscription.no_plan_hint') }}

    +
    + @endif + + + {{-- INTERNE USER: Kein Plan-Wechsel --}} + @if(auth()->user()->isInternalUser()) +
    +

    {{ t('subscription.internal') }}

    +
    + @else + + {{-- PLAN WECHSELN --}} +
    + +
    +

    {{ t('subscription.change_plan') }}

    +
    + + +
    +
    + + {{-- FIX 1: Cards zentriert mit max-width --}} +
    + @foreach($plans as $plan) + @php + $isCurrent = $subscription && $subscription->plan_id === $plan->id; + $priceMonthly = $plan->price / 100; + $priceYearly = ($plan->price * (12 - $plan->yearly_discount_months)) / 100; + $perMonth = $priceYearly / 12; + $isUpgrade = !$subscription || ($subscription->plan && $plan->sort > $subscription->plan->sort); + @endphp + + + @endforeach +
    + +
    + + {{-- FEATURE-VERGLEICHSTABELLE --}} + @php + $featureGroups = $allFeatures->groupBy('feature_group_id'); + $groupModels = \App\Models\FeatureGroup::whereIn('id', $allFeatures->pluck('feature_group_id')->unique()) + ->orderBy('sort')->get()->keyBy('id'); + @endphp +
    + + {{-- Header --}} +
    +
    {{ t('subscription.features') }}
    + @foreach($plans as $plan) + + @endforeach +
    + + @foreach($groupModels as $groupId => $group) + @php $gFeatures = ($featureGroups[$groupId] ?? collect())->sortBy('sort'); @endphp + @if($gFeatures->isEmpty()) @continue @endif + + {{-- Gruppen-Header --}} +
    +
    {{ $group->label }}
    + @foreach($plans as $plan) + + @endforeach +
    + + {{-- Feature-Zeilen --}} + @foreach($gFeatures as $feature) +
    +
    {{ $feature->label }}
    + @foreach($plans as $plan) + + @endforeach +
    + @endforeach + @endforeach + + {{-- Footer mit Buttons --}} +
    +
    + @foreach($plans as $plan) + @php + $isCurrent = $subscription && $subscription->plan_id === $plan->id; + $isUpgrade = !$subscription || ($subscription->plan && $plan->sort > $subscription->plan->sort); + @endphp + + @endforeach +
    + +
    + +
    + + + {{ t('subscription.cancel_anytime') }} + + + + {{ t('subscription.no_hidden_costs') }} + + + + {{ t('subscription.instant_active') }} + +
    + +
    + + @endif {{-- Ende: !isInternalUser --}} + +
    diff --git a/src/resources/views/livewire/tasks/index.blade.php b/src/resources/views/livewire/tasks/index.blade.php new file mode 100644 index 0000000..fb4fb3d --- /dev/null +++ b/src/resources/views/livewire/tasks/index.blade.php @@ -0,0 +1,346 @@ +
    + + {{-- HEADER --}} +
    +
    +

    {{ t('tasks.title') }}

    +

    {{ t('tasks.subtitle') }}

    +
    + +
    + + + {{-- STATS --}} + @if($stats && $stats->total > 0) +
    + @foreach([ + ['label' => t('tasks.total'), 'value' => $stats->total, 'icon' => 'squares-2x2', 'color' => 'text-gray-600', 'bg' => 'bg-gray-100'], + ['label' => t('tasks.open'), 'value' => $stats->open, 'icon' => 'clock', 'color' => 'text-amber-600', 'bg' => 'bg-amber-50'], + ['label' => t('tasks.today'), 'value' => $stats->today, 'icon' => 'sun', 'color' => 'text-indigo-600','bg' => 'bg-indigo-50'], + ['label' => t('tasks.done'), 'value' => $stats->done, 'icon' => 'check-circle', 'color' => 'text-green-600', 'bg' => 'bg-green-50'], + ] as $stat) +
    +
    + @if($stat['icon'] === 'squares-2x2') + @elseif($stat['icon'] === 'clock') + @elseif($stat['icon'] === 'sun') + @else + @endif +
    +
    +

    {{ number_format($stat['value']) }}

    +

    {{ $stat['label'] }}

    +
    +
    + @endforeach +
    + @endif + + + {{-- SUCHE + STATUSFILTER --}} +
    +
    + + +
    +
    + @foreach([ + ['' , t('common.all'), 'indigo'], + ['pending' , t('tasks.open'), 'amber'], + ['in_progress', t('tasks.in_progress'), 'blue'], + ['today' , t('tasks.due_today'), 'red'], + ['done' , t('tasks.done'), 'green'], + ] as [$key, $label, $col]) + @php + $active = $filterStatus === $key; + $cls = $active + ? "bg-{$col}-600 text-white border-{$col}-600" + : "bg-white text-gray-500 border-gray-200 hover:border-gray-300 hover:text-gray-700"; + if ($col === 'indigo' && !$active) $cls = 'bg-white text-gray-500 border-gray-200 hover:border-gray-300 hover:text-gray-700'; + if ($col === 'indigo' && $active) $cls = 'bg-indigo-600 text-white border-indigo-600'; + @endphp + + @endforeach +
    +
    + + + {{-- FORMULAR --}} + @if($showForm) +
    + +
    +

    {{ $editId ? t('tasks.edit') : t('tasks.new') }}

    + +
    + + + @error('taskTitle')

    {{ $message }}

    @enderror + + + +
    +
    + +
    + @foreach([ + ['key' => 'low', 'label' => t('tasks.priority_low'), 'active' => 'bg-green-500 text-white border-green-500', 'inactive' => 'bg-green-50 text-green-600 border-green-200'], + ['key' => 'medium', 'label' => t('tasks.priority_medium'), 'active' => 'bg-amber-500 text-white border-amber-500', 'inactive' => 'bg-amber-50 text-amber-600 border-amber-200'], + ['key' => 'high', 'label' => t('tasks.priority_high'), 'active' => 'bg-red-500 text-white border-red-500', 'inactive' => 'bg-red-50 text-red-600 border-red-200'], + ] as $p) + + @endforeach +
    +
    +
    + + +
    +
    + + {{-- Reminder Section --}} +
    + + + @if($reminderAt) +
    + + + {{ \Carbon\Carbon::createFromFormat('Y-m-d\TH:i', $reminderAt, auth()->user()->timezone ?? 'Europe/Vienna')->format('d.m.Y H:i') }} Uhr + + +
    + @endif + +
    + + + + +
    + +
    + +
    + +
    +
    + + @error('reminderAt') +
    + +

    + {{ $message }} + @if(auth()->user()->subscription?->plan?->plan_key === 'free' || !auth()->user()->subscription) + {{ t('tasks.limit_upgrade') }} + @endif +

    +
    + @enderror + + @if(auth()->user()->subscription?->plan?->plan_key === 'free' || !auth()->user()->subscription) +

    {{ t('tasks.reminder_limit_info') }}

    + @endif +
    + +
    + + +
    + +
    + @endif + + + {{-- AUFGABENLISTE --}} + @if($tasks->isEmpty()) + +
    +
    + +
    +

    {{ t('tasks.no_tasks') }}

    + @if($search || $filterStatus) + + @else + + @endif +
    + + @else + + {{-- ── Offene Aufgaben ── --}} + @if($openTasks->isNotEmpty()) +
    + @foreach($openTasks as $task) + @php $p = $task->priorityClasses(); @endphp + +
    + + + +
    +
    +
    +

    {{ $task->title }}

    + @if($task->description) +

    {{ $task->description }}

    + @endif +
    + +
    + @if($task->due_at) + + {{ $task->due_at->format('d.m.Y') }} + @if($task->isOverdue()) (!!) @endif + + @endif + + +
    +
    + +
    + + + {{ $task->priorityLabel() }} + + @if($task->status === 'in_progress') + + {{ t('tasks.in_progress') }} + + @endif + @if($task->reminder_at && !$task->reminder_sent && $task->reminder_at->isFuture()) + + + {{ $task->reminder_at->setTimezone(auth()->user()->timezone ?? 'Europe/Vienna')->format('d.m. H:i') }} + + @endif + @if($task->status === 'pending') + + @endif +
    +
    +
    + @endforeach +
    + @endif + + {{-- ── Erledigte Aufgaben (eigene Section) ── --}} + @if($doneTasks->isNotEmpty()) +
    + + +
    +
    + @foreach($doneTasks as $task) +
    + + + +
    +
    +

    {{ $task->title }}

    +
    + @if($task->completed_at) + + {{ $task->completed_at->format('d.m.Y') }} + + @endif + +
    +
    +
    +
    + @endforeach +
    +
    +
    + @endif + + @endif + +
    diff --git a/src/resources/views/partials/homepage/footer.blade.php b/src/resources/views/partials/homepage/footer.blade.php new file mode 100644 index 0000000..6966ef6 --- /dev/null +++ b/src/resources/views/partials/homepage/footer.blade.php @@ -0,0 +1,95 @@ +{{-- Homepage Footer --}} +
    +
    + +
    + + {{-- Brand --}} +
    +
    +
    + +
    + aziros +
    +

    + Dein KI-gestΓΌtzter ProduktivitΓ€ts-Hub. Kalender, Aufgaben und Assistent β€” alles in einer Plattform. +

    +
    + + {{-- Produkt --}} + + + {{-- Unternehmen --}} +
    +

    Unternehmen

    + +
    + + {{-- Rechtliches --}} +
    +

    Rechtliches

    + +
    + + {{-- App herunterladen --}} +
    +

    App herunterladen

    +
      +
    • +
      + + + + + App Store (iOS) + + Bald +
      +
    • +
    • +
      + + + + + + + + Google Play + + Bald +
      +
    • +
    +
    + +
    + + {{-- Bottom bar --}} +
    + © {{ date('Y') }} aziros GmbH. Alle Rechte vorbehalten. + +
    + +
    +
    diff --git a/src/resources/views/partials/homepage/navbar.blade.php b/src/resources/views/partials/homepage/navbar.blade.php new file mode 100644 index 0000000..aed5b47 --- /dev/null +++ b/src/resources/views/partials/homepage/navbar.blade.php @@ -0,0 +1,37 @@ +{{-- Homepage Navbar --}} + diff --git a/src/resources/views/vendor/wire-elements-modal/modal.blade.php b/src/resources/views/vendor/wire-elements-modal/modal.blade.php new file mode 100644 index 0000000..6437ea6 --- /dev/null +++ b/src/resources/views/vendor/wire-elements-modal/modal.blade.php @@ -0,0 +1,57 @@ +
    + @isset($jsPath) + + @endisset + @isset($cssPath) + + @endisset + + +
    diff --git a/src/resources/views/welcome.blade.php b/src/resources/views/welcome.blade.php new file mode 100644 index 0000000..9873517 --- /dev/null +++ b/src/resources/views/welcome.blade.php @@ -0,0 +1,225 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + +
    + @if (Route::has('login')) + + @endif +
    +
    +
    +
    +

    Let's get started

    +

    With so many options available to you,
    we suggest you start with the following:

    + + + +

    + v{{ app()->version() }} + + View changelog + + + + +

    +
    +
    + {{-- Laravel Logo --}} + + + + + + + + + + + {{-- 13 --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + @if (Route::has('login')) + + @endif + + diff --git a/src/routes/api.php b/src/routes/api.php new file mode 100644 index 0000000..5f7bad2 --- /dev/null +++ b/src/routes/api.php @@ -0,0 +1,163 @@ +name('stripe.webhook'); + +Route::post('/webhooks/google-calendar', GoogleCalendarWebhookController::class) + ->name('webhooks.google-calendar'); + +Route::prefix('v1')->group(function () { + + // Translations β€” ΓΆffentlich, kein Auth + Route::get('/translations/{locale}', [TranslationController::class, 'index']); + + // Version β€” ΓΆffentlich, kein Auth + Route::get('/version/current', function (\Illuminate\Http\Request $request) { + $platform = $request->query('platform', 'web'); + $version = \App\Models\AppVersion::current($platform); + + return response()->json([ + 'success' => true, + 'data' => $version ? [ + 'version' => $version->version, + 'name' => $version->name, + 'changelog' => $version->changelog, + 'status' => $version->status, + 'show_popup' => $version->show_popup, + 'released_at' => $version->released_at, + ] : null, + ]); + }); + + // Auth β€” Login ohne Token + Route::post('/auth/login', [AuthController::class, 'login']); + + // GeschΓΌtzte API-Routen β€” Bearer Token oder Session erforderlich + Route::middleware('auth.custom')->group(function () { + + // Auth + Route::post('/auth/logout', [AuthController::class, 'logout']); + Route::get('/auth/me', [AuthController::class, 'me']); + + // Kalender + Route::get('/events', [EventController::class, 'index']); + Route::post('/events', [EventController::class, 'store']); + Route::put('/events/{id}', [EventController::class, 'update']); + Route::delete('/events/{id}', [EventController::class, 'destroy']); + + // Aufgaben + Route::get('/tasks', [TaskController::class, 'index']); + Route::post('/tasks', [TaskController::class, 'store']); + Route::put('/tasks/{id}', [TaskController::class, 'update']); + Route::delete('/tasks/{id}', [TaskController::class, 'destroy']); + + // Kontakte + Route::get('/contacts', [ContactController::class, 'index']); + Route::post('/contacts', [ContactController::class, 'store']); + Route::put('/contacts/{id}', [ContactController::class, 'update']); + Route::delete('/contacts/{id}', [ContactController::class, 'destroy']); + + // Notizen + Route::get('/notes', [NoteController::class, 'index']); + Route::post('/notes', [NoteController::class, 'store']); + Route::put('/notes/{id}', [NoteController::class, 'update']); + Route::delete('/notes/{id}', [NoteController::class, 'destroy']); + + // Aria Agent + Route::post('/agent/chat', [AgentChatController::class, 'chat']); + Route::post('/agent/synthesize', [AgentChatController::class, 'synthesize']); + Route::get('/agent/logs', [AgentChatController::class, 'logs']); + + // Einstellungen + Route::get('/settings/credits', [SettingsController::class, 'credits']); + Route::get('/settings/affiliate', [SettingsController::class, 'affiliate']); + Route::put('/settings/profile', [SettingsController::class, 'updateProfile']); + Route::put('/settings/password', [SettingsController::class, 'updatePassword']); + Route::delete('/settings/account', [SettingsController::class, 'deleteAccount']); + Route::get('/settings/notifications', [SettingsController::class, 'notificationSettings']); + Route::put('/settings/notifications', [SettingsController::class, 'notificationSettings']); + + // Automationen + Route::get('/automations', [AutomationController::class, 'index']); + Route::post('/automations/{type}/toggle', [AutomationController::class, 'toggle']); + Route::put('/automations/{type}', [AutomationController::class, 'update']); + Route::delete('/automations/{type}', [AutomationController::class, 'destroy']); + + // Dashboard + Route::get('/dashboard/birthdays', [DashboardController::class, 'birthdays']); + + // In-App Notifications + Route::get('/notifications/unread', function () { + $userId = auth()->id(); + $notifications = \App\Models\Notification::where('user_id', $userId) + ->latest() + ->take(15) + ->get(); + return response()->json([ + 'success' => true, + 'data' => [ + 'notifications' => $notifications, + 'count' => \App\Models\Notification::where('user_id', $userId)->whereNull('read_at')->count(), + ], + ]); + }); + Route::post('/notifications/read-all', function () { + \App\Models\Notification::where('user_id', auth()->id()) + ->whereNull('read_at') + ->update(['read_at' => now()]); + return response()->json(['success' => true]); + }); + Route::post('/notifications/{id}/read', function (string $id) { + \App\Models\Notification::where('user_id', auth()->id()) + ->where('id', $id) + ->update(['read_at' => now()]); + return response()->json(['success' => true]); + }); + Route::delete('/notifications/read', function () { + \App\Models\Notification::where('user_id', auth()->id()) + ->whereNotNull('read_at') + ->delete(); + return response()->json(['success' => true]); + }); + Route::delete('/notifications', function () { + \App\Models\Notification::where('user_id', auth()->id())->delete(); + return response()->json(['success' => true]); + }); + Route::delete('/notifications/{id}', function (string $id) { + \App\Models\Notification::where('user_id', auth()->id()) + ->where('id', $id) + ->delete(); + return response()->json(['success' => true]); + }); + + // Push Notifications / GerΓ€te + Route::post('/devices/register', [DeviceController::class, 'register']); + Route::put('/devices/{device_id}/token', [DeviceController::class, 'updateToken']); + Route::delete('/devices/{device_id}/token', [DeviceController::class, 'deactivateToken']); + Route::delete('/devices/{device_id}', [DeviceController::class, 'destroy']); + }); + +}); + +// Fallback β€” fΓ€ngt alle nicht definierten Routen auf api.aziros.com ab +Route::fallback(function () { + return response()->json([ + 'success' => false, + 'message' => 'Not Found', + ], 404); +}); diff --git a/src/routes/channels.php b/src/routes/channels.php new file mode 100644 index 0000000..ae8f4ba --- /dev/null +++ b/src/routes/channels.php @@ -0,0 +1,15 @@ +id === (int) $id; +}); + +Broadcast::channel('calendar.{userId}', function ($user, $userId) { + return $user->id === $userId; +}); + +Broadcast::channel('notifications.{userId}', function ($user, $userId) { + return (string) $user->id === $userId; +}); diff --git a/src/routes/connect.php b/src/routes/connect.php new file mode 100644 index 0000000..8121bf8 --- /dev/null +++ b/src/routes/connect.php @@ -0,0 +1,14 @@ +name('integrations.google.redirect'); +Route::get('/integrations/google/callback', [GoogleCalendarController::class, 'callback'])->name('integrations.google.callback'); +Route::get('/integrations/outlook/redirect', [OutlookCalendarController::class, 'redirect'])->name('integrations.outlook.redirect'); +Route::get('/integrations/outlook/callback', [OutlookCalendarController::class, 'callback'])->name('integrations.outlook.callback'); + +// Unbekannte Routen auf app.aziros.com weiterleiten (gleicher Pfad) +Route::fallback(function (\Illuminate\Http\Request $request) { + return redirect()->to('https://app.aziros.com' . $request->getRequestUri()); +}); diff --git a/src/routes/console.php b/src/routes/console.php new file mode 100644 index 0000000..676a358 --- /dev/null +++ b/src/routes/console.php @@ -0,0 +1,45 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); + +Schedule::command('users:cleanup-deleted')->daily(); +Schedule::command('subscriptions:check-expired')->daily(); +Schedule::command('affiliates:process-qualifications')->daily(); +Schedule::command('credits:monthly-reset')->monthlyOn(1, '00:05'); +Schedule::command('mail:cleanup')->everyMinute(); + +// Google Calendar Pull-Sync: alle 5 Minuten +// Push-Webhook via Google Watch ist registriert aber kann je nach Firewall/Cloudflare +// blockiert werden β€” 5-Minuten-Polling ist der zuverlΓ€ssige Fallback +Schedule::call(function () { + \App\Models\CalendarIntegration::where('provider', 'google') + ->whereIn('sync_mode', ['read', 'both']) + ->each(function ($integration) { + \App\Jobs\SyncFromGoogleCalendarJob::dispatch($integration->id); + }); +})->everyFiveMinutes()->name('google-calendar-sync'); + +// Event Reminders: jede Minute, Dedup via sent_reminders Tabelle +Schedule::command('reminders:schedule')->everyMinute(); + +// Allgemeine Automationen (Geburtstag, Agenda, etc.): jede Minute, intern zeitgesteuert +Schedule::command('automations:run')->everyMinute(); + +// Google Watch erneuern: tΓ€glich Watches prΓΌfen die in 24h ablaufen +Schedule::call(function () { + \App\Models\CalendarIntegration::where('provider', 'google') + ->whereIn('sync_mode', ['read', 'both']) + ->where(function ($q) { + $q->whereNull('watch_expires_at') + ->orWhere('watch_expires_at', '<=', now()->addDay()); + }) + ->each(function ($integration) { + app(\App\Services\GoogleCalendarService::class)->createWatch($integration); + }); +})->daily()->name('google-calendar-watch-renewal'); diff --git a/src/routes/web.php b/src/routes/web.php new file mode 100644 index 0000000..d319bd3 --- /dev/null +++ b/src/routes/web.php @@ -0,0 +1,93 @@ + 'Max Mustermann', + 'code' => '123456', + 'url' => 'https://example.com/verify' // πŸ”₯ hinzufΓΌgen + ]); +}); + +Route::middleware(['auth.custom'])->group(function () { + // intentionally empty β€” kept for middleware reference +}); + +Route::middleware('guest.custom')->group(function () { + Route::get('/login', Login::class)->name('login'); + Route::get('/register', Register::class)->name('register'); + + Route::get('/password/reset', ForgotPassword::class)->name('password.request'); + Route::get('/password/reset/{token}', ResetPassword::class)->name('password.reset'); + + Route::get('/verify/{verification}', [VerifyController::class, 'verify']) + ->name('verify.user') + ->middleware('signed'); + + Route::get('/verify-notice/{user}', VerifyNotice::class)->name('verify.notice'); +}); + + +// Admin-Bereich: Support+ kann Dashboard & User sehen +Route::middleware(['user', 'role:support'])->prefix('admin')->name('admin.')->group(function () { + Route::get('/', \App\Livewire\Admin\Dashboard::class)->name('dashboard'); + Route::get('/users', \App\Livewire\Admin\Users\Index::class)->name('users.index'); + Route::get('/users/{user}', \App\Livewire\Admin\Users\Detail::class)->name('users.detail'); + Route::get('/users/{user}/calendar', \App\Livewire\Admin\Users\Calendar::class)->name('users.calendar'); +}); + +// Admin-Bereich: Admin+ kann PlΓ€ne, Affiliates, Übersetzungen verwalten +Route::middleware(['user', 'role:admin'])->prefix('admin')->name('admin.')->group(function () { + Route::get('/plans', \App\Livewire\Admin\Plans\Index::class)->name('plans.index'); + Route::get('/affiliates', \App\Livewire\Admin\Affiliates\Index::class)->name('affiliates.index'); + Route::get('/translations', TranslationIndex::class)->name('translations.index'); + Route::get('/versions', \App\Livewire\Admin\Versions::class)->name('versions.index'); +}); + +Route::middleware('user')->group(function () { + + Route::get('/', App\Livewire\Dashboard\Index::class)->name('dashboard.index'); + Route::get('/calendar', \App\Livewire\Calendar\WeekCanvas::class)->name('calendar.index'); + Route::get('/agent', AgentIndex::class)->name('agent.index'); + Route::get('/agent/logs', \App\Livewire\Agent\Logs::class)->name('agent.logs'); + Route::get('/activities', ActivitiesIndex::class)->name('activities.index'); + Route::get('/notes', \App\Livewire\Notes\Index::class)->name('notes.index'); + Route::get('/tasks', \App\Livewire\Tasks\Index::class)->name('tasks.index'); + Route::get('/contacts', ContactsIndex::class)->name('contacts.index'); + Route::get('/plans', PlansIndex::class)->name('plans.index'); + Route::get('/subscription', \App\Livewire\Subscription\Index::class)->name('subscription.index'); + Route::get('/invoices', \App\Livewire\Invoices\Index::class)->name('invoices.index'); + Route::get('/checkout/success', \App\Livewire\Checkout\Success::class)->name('checkout.success'); + Route::get('/checkout/{planId}/{billing?}', \App\Livewire\Checkout\Index::class)->name('checkout.index'); + Route::get('/integrations', IntegrationIndex::class)->name('integrations.index'); + Route::get('/automations', AutomationIndex::class)->name('automations.index'); + Route::get('/settings', SettingsIndex::class)->name('settings.index'); + Route::get('/settings/affiliate', fn() => redirect()->route('settings.index', ['tab' => 'affiliate']))->name('settings.affiliate'); + + Route::post('/logout', function () { + auth()->logout(); + return redirect('/'); + + })->name('logout'); +}); diff --git a/src/routes/www.php b/src/routes/www.php new file mode 100644 index 0000000..7ec5c02 --- /dev/null +++ b/src/routes/www.php @@ -0,0 +1,14 @@ +name('homepage.index'); +Route::get('/pricing', App\Livewire\Homepage\Preise::class)->name('homepage.preise'); +Route::get('/about', App\Livewire\Homepage\UeberUns::class)->name('homepage.ueber-uns'); +Route::get('/contact', App\Livewire\Homepage\Kontakt::class)->name('homepage.kontakt'); +Route::get('/privacy', App\Livewire\Homepage\Datenschutz::class)->name('homepage.datenschutz'); +Route::get('/imprint', App\Livewire\Homepage\Impressum::class)->name('homepage.impressum'); +Route::get('/terms', App\Livewire\Homepage\Agb::class)->name('homepage.agb'); +Route::get('/example', App\Livewire\Homepage\Example::class)->name('homepage.example'); diff --git a/src/storage/app/.gitignore b/src/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/src/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/src/storage/app/private/.gitignore b/src/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/src/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/storage/app/public/.gitignore b/src/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/src/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/storage/framework/.gitignore b/src/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/src/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/src/storage/framework/testing/.gitignore b/src/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/src/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/tests/Feature/AutomationTest.php b/src/tests/Feature/AutomationTest.php new file mode 100644 index 0000000..ade323d --- /dev/null +++ b/src/tests/Feature/AutomationTest.php @@ -0,0 +1,467 @@ +user = User::factory()->create(); + $this->actingAs($this->user); + } + + // ── Model Tests ────────────────────────────────────────────── + + public function test_automation_can_be_created(): void + { + $automation = Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Termin-Erinnerung', + 'active' => true, + 'config' => ['minutes_before' => 30, 'channel' => 'in_app'], + ]); + + $this->assertDatabaseHas('automations', [ + 'id' => $automation->id, + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'active' => true, + ]); + } + + public function test_automation_has_uuid_primary_key(): void + { + $automation = Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'daily_agenda', + 'name' => 'TΓ€gliche Agenda', + 'active' => true, + 'config' => ['send_time' => '08:00', 'channel' => 'email'], + ]); + + $this->assertNotNull($automation->id); + $this->assertMatchesRegularExpression( + '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', + $automation->id + ); + } + + public function test_automation_config_is_cast_to_array(): void + { + $automation = Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Test', + 'active' => true, + 'config' => ['minutes_before' => 15, 'channel' => 'email'], + ]); + + $automation->refresh(); + $this->assertIsArray($automation->config); + $this->assertEquals(15, $automation->config['minutes_before']); + $this->assertEquals('email', $automation->config['channel']); + } + + public function test_cfg_helper_returns_config_values(): void + { + $automation = Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Test', + 'active' => true, + 'config' => ['minutes_before' => 60, 'channel' => 'in_app'], + ]); + + $this->assertEquals(60, $automation->cfg('minutes_before')); + $this->assertEquals('in_app', $automation->cfg('channel')); + $this->assertNull($automation->cfg('nonexistent')); + $this->assertEquals('fallback', $automation->cfg('nonexistent', 'fallback')); + } + + public function test_automation_belongs_to_user(): void + { + $automation = Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'daily_summary', + 'name' => 'Test', + 'active' => true, + 'config' => [], + ]); + + $this->assertTrue($automation->user->is($this->user)); + } + + public function test_user_has_many_automations(): void + { + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'A', + 'active' => true, + 'config' => [], + ]); + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'daily_agenda', + 'name' => 'B', + 'active' => false, + 'config' => [], + ]); + + $this->assertCount(2, $this->user->automations); + } + + public function test_unique_constraint_on_user_and_type(): void + { + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Original', + 'active' => true, + 'config' => [], + ]); + + $this->expectException(\Illuminate\Database\QueryException::class); + + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Duplikat', + 'active' => true, + 'config' => [], + ]); + } + + public function test_different_users_can_have_same_type(): void + { + $otherUser = User::factory()->create(); + + $a1 = Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'User 1', + 'active' => true, + 'config' => [], + ]); + + $a2 = Automation::create([ + 'user_id' => $otherUser->id, + 'type' => 'event_reminder', + 'name' => 'User 2', + 'active' => true, + 'config' => [], + ]); + + $this->assertDatabaseCount('automations', 2); + $this->assertNotEquals($a1->id, $a2->id); + } + + // ── Livewire Component Tests ───────────────────────────────── + + public function test_automation_page_loads(): void + { + // User braucht role fΓΌr Sidebar-Rendering (hasAtLeastRole) + $this->user->update(['role' => 'user']); + $response = $this->get(route('automations.index')); + $response->assertStatus(200); + } + + public function test_all_automation_types_are_defined(): void + { + $types = \App\Livewire\Automation\Index::TYPES; + + $this->assertCount(9, $types); + + $expectedTypes = [ + 'event_reminder', 'daily_agenda', 'weekly_overview', + 'event_followup', 'birthday_reminder', 'no_activity_reminder', + 'daily_summary', 'contact_followup', 'free_slots_report', + ]; + + foreach ($expectedTypes as $type) { + $this->assertArrayHasKey($type, $types); + $this->assertArrayHasKey('name', $types[$type]); + $this->assertArrayHasKey('description', $types[$type]); + $this->assertArrayHasKey('icon', $types[$type]); + $this->assertArrayHasKey('color', $types[$type]); + $this->assertArrayHasKey('defaults', $types[$type]); + } + } + + public function test_toggle_creates_automation_if_not_exists(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->call('toggle', 'event_reminder'); + + $this->assertDatabaseHas('automations', [ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'active' => true, + ]); + } + + public function test_toggle_deactivates_existing_automation(): void + { + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'daily_agenda', + 'name' => 'TΓ€gliche Agenda', + 'active' => true, + 'config' => ['send_time' => '08:00', 'channel' => 'email'], + ]); + + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->call('toggle', 'daily_agenda'); + + $this->assertDatabaseHas('automations', [ + 'user_id' => $this->user->id, + 'type' => 'daily_agenda', + 'active' => false, + ]); + } + + public function test_toggle_reactivates_inactive_automation(): void + { + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'weekly_overview', + 'name' => 'WΓΆchentliche Vorschau', + 'active' => false, + 'config' => [], + ]); + + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->call('toggle', 'weekly_overview'); + + $this->assertDatabaseHas('automations', [ + 'user_id' => $this->user->id, + 'type' => 'weekly_overview', + 'active' => true, + ]); + } + + public function test_save_creates_automation_with_config(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->set('activeType', 'event_reminder') + ->set('f_minutes_before', '15') + ->set('f_channel', 'email') + ->call('save'); + + $automation = Automation::where('user_id', $this->user->id) + ->where('type', 'event_reminder') + ->first(); + + $this->assertNotNull($automation); + $this->assertTrue($automation->active); + $this->assertEquals(15, $automation->config['minutes_before']); + $this->assertEquals('email', $automation->config['channel']); + } + + public function test_save_updates_existing_automation(): void + { + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Termin-Erinnerung', + 'active' => true, + 'config' => ['minutes_before' => 30, 'channel' => 'in_app'], + ]); + + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->set('activeType', 'event_reminder') + ->set('f_minutes_before', '60') + ->set('f_channel', 'email') + ->call('save'); + + $automation = Automation::where('user_id', $this->user->id) + ->where('type', 'event_reminder') + ->first(); + + $this->assertEquals(60, $automation->config['minutes_before']); + $this->assertEquals('email', $automation->config['channel']); + $this->assertDatabaseCount('automations', 1); + } + + public function test_save_ignores_invalid_type(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->set('activeType', 'nonexistent_type') + ->call('save'); + + $this->assertDatabaseCount('automations', 0); + } + + public function test_delete_removes_automation(): void + { + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Test', + 'active' => true, + 'config' => [], + ]); + + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->call('delete', 'event_reminder'); + + $this->assertDatabaseMissing('automations', [ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + ]); + } + + public function test_delete_does_not_affect_other_users(): void + { + $otherUser = User::factory()->create(); + + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Mine', + 'active' => true, + 'config' => [], + ]); + Automation::create([ + 'user_id' => $otherUser->id, + 'type' => 'event_reminder', + 'name' => 'Others', + 'active' => true, + 'config' => [], + ]); + + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->call('delete', 'event_reminder'); + + $this->assertDatabaseMissing('automations', [ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + ]); + $this->assertDatabaseHas('automations', [ + 'user_id' => $otherUser->id, + 'type' => 'event_reminder', + ]); + } + + public function test_open_panel_loads_defaults_for_new_automation(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->call('openPanel', 'event_reminder') + ->assertSet('panelOpen', true) + ->assertSet('activeType', 'event_reminder') + ->assertSet('f_minutes_before', '30') + ->assertSet('f_channel', 'in_app'); + } + + public function test_open_panel_loads_saved_config(): void + { + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Termin-Erinnerung', + 'active' => true, + 'config' => ['minutes_before' => 45, 'channel' => 'email'], + ]); + + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->call('openPanel', 'event_reminder') + ->assertSet('f_minutes_before', '45') + ->assertSet('f_channel', 'email'); + } + + public function test_close_panel_resets_state(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->call('openPanel', 'event_reminder') + ->call('closePanel') + ->assertSet('panelOpen', false) + ->assertSet('activeType', ''); + } + + // ── Config-Builder fΓΌr alle Typen ──────────────────────────── + + public function test_save_daily_agenda_config(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->set('activeType', 'daily_agenda') + ->set('f_send_time', '07:30') + ->set('f_channel', 'email') + ->set('f_weekdays_only', true) + ->call('save'); + + $a = Automation::where('user_id', $this->user->id)->where('type', 'daily_agenda')->first(); + $this->assertEquals('07:30', $a->config['send_time']); + $this->assertTrue($a->config['weekdays_only']); + } + + public function test_save_weekly_overview_config(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->set('activeType', 'weekly_overview') + ->set('f_send_day', '5') + ->set('f_send_time', '09:00') + ->set('f_channel', 'email') + ->call('save'); + + $a = Automation::where('user_id', $this->user->id)->where('type', 'weekly_overview')->first(); + $this->assertEquals(5, $a->config['send_day']); + $this->assertEquals('09:00', $a->config['send_time']); + } + + public function test_save_event_followup_config(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->set('activeType', 'event_followup') + ->set('f_delay_minutes', '60') + ->set('f_create_activity', false) + ->set('f_channel', 'email') + ->call('save'); + + $a = Automation::where('user_id', $this->user->id)->where('type', 'event_followup')->first(); + $this->assertEquals(60, $a->config['delay_minutes']); + $this->assertFalse($a->config['create_activity']); + } + + public function test_save_free_slots_report_config(): void + { + \Livewire\Livewire::test(\App\Livewire\Automation\Index::class) + ->set('activeType', 'free_slots_report') + ->set('f_send_day', '7') + ->set('f_send_time', '17:00') + ->set('f_min_slot_minutes', '90') + ->set('f_channel', 'email') + ->call('save'); + + $a = Automation::where('user_id', $this->user->id)->where('type', 'free_slots_report')->first(); + $this->assertEquals(90, $a->config['min_slot_minutes']); + $this->assertEquals(7, $a->config['send_day']); + } + + // ── Cascade Delete ─────────────────────────────────────────── + + public function test_automations_are_deleted_when_user_is_deleted(): void + { + Automation::create([ + 'user_id' => $this->user->id, + 'type' => 'event_reminder', + 'name' => 'Test', + 'active' => true, + 'config' => [], + ]); + + $this->user->forceDelete(); + + $this->assertDatabaseCount('automations', 0); + } +} diff --git a/src/tests/Feature/ExampleTest.php b/src/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8364a84 --- /dev/null +++ b/src/tests/Feature/ExampleTest.php @@ -0,0 +1,19 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/src/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/src/vite.config.js b/src/vite.config.js new file mode 100644 index 0000000..b9bbb5d --- /dev/null +++ b/src/vite.config.js @@ -0,0 +1,53 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + tailwindcss(), + ], + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + // Alpine.js eigener Chunk + if (id.includes('alpinejs')) return 'alpine'; + // Axios eigener Chunk + if (id.includes('axios')) return 'axios'; + // Alles andere aus node_modules β†’ vendor + return 'vendor'; + } + }, + }, + }, + chunkSizeWarningLimit: 600, + }, + server: { + host: '0.0.0.0', + port: 5173, + + cors: true, + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, + + // server: { + // host: '0.0.0.0', + // port: 5173, + // + // hmr: { + // host: 'localhost', + // protocol: 'ws', + // }, + // cors: true, + // watch: { + // ignored: ['**/storage/framework/views/**'], + // }, + // }, +});

    &O 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 b_FYcvul}W)=fo3kK|1@V=H|Bi_!ySgj0 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 BK-T~O literal 0 HcmV?d00001 diff --git a/src/resources/fonts/Syne/SyneMono-Regular.woff2 b/src/resources/fonts/Syne/SyneMono-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..06ac0db62fec8bd0bc2dae564fe3afe360a17baf GIT binary patch literal 27276 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 literal 0 HcmV?d00001 diff --git a/src/resources/fonts/Syne/font.css b/src/resources/fonts/Syne/font.css new file mode 100644 index 0000000..0527747 --- /dev/null +++ b/src/resources/fonts/Syne/font.css @@ -0,0 +1,32 @@ +/* bai-jamjuree-200italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Syne'; + font-style: italic; + font-weight: 400; + src: url('Syne-Italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-regular - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Syne'; + font-style: normal; + font-weight: 400; + src: url('Syne-Regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-600 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Syne'; + font-style: normal; + font-weight: 600; + src: url('Syne-Bold.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} +/* bai-jamjuree-600italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Syne'; + font-style: normal; + font-weight: 800; + src: url('Syne-ExtraBold.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} diff --git a/src/resources/js/agent-voice-orb.js b/src/resources/js/agent-voice-orb.js new file mode 100644 index 0000000..8f77c2c --- /dev/null +++ b/src/resources/js/agent-voice-orb.js @@ -0,0 +1,523 @@ +/** + * Agent Voice Orb β€” Three.js 3D Blob Sphere + * + * Glasartige 3D-Kugel mit Noise-Displacement, Rotation und Audio-ReaktivitΓ€t. + * Three.js Objekte werden AUSSERHALB von Alpine gespeichert um Proxy-Konflikte zu vermeiden. + */ +import { WebGLRenderer, Scene, PerspectiveCamera, IcosahedronGeometry, ShaderMaterial, Mesh, Color, DoubleSide } from 'three'; + +// ── Three.js State (NICHT reaktiv, kein Alpine Proxy) ──────────────── +const _threeState = new WeakMap(); + +function getThree(el) { return _threeState.get(el) || null; } +function setThree(el, state) { _threeState.set(el, state); } +function clearThree(el) { _threeState.delete(el); } + +// ── Vertex Shader ──────────────────────────────────────────────────── +const vertexShader = ` + uniform float uTime; + uniform float uDisplacement; + uniform float uSpeed; + varying vec3 vNormal; + varying vec3 vViewPos; + varying float vDisp; + + vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;} + vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;} + vec4 permute(vec4 x){return mod289((x*34.0+1.0)*x);} + vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;} + + float snoise(vec3 v){ + const vec2 C=vec2(1.0/6.0,1.0/3.0); + const vec4 D=vec4(0.0,0.5,1.0,2.0); + vec3 i=floor(v+dot(v,C.yyy)); + vec3 x0=v-i+dot(i,C.xxx); + vec3 g=step(x0.yzx,x0.xyz); + vec3 l=1.0-g; + vec3 i1=min(g.xyz,l.zxy); + vec3 i2=max(g.xyz,l.zxy); + vec3 x1=x0-i1+C.xxx; + vec3 x2=x0-i2+C.yyy; + vec3 x3=x0-D.yyy; + i=mod289(i); + vec4 p=permute(permute(permute( + i.z+vec4(0.0,i1.z,i2.z,1.0)) + +i.y+vec4(0.0,i1.y,i2.y,1.0)) + +i.x+vec4(0.0,i1.x,i2.x,1.0)); + float n_=0.142857142857; + vec3 ns=n_*D.wyz-D.xzx; + vec4 j=p-49.0*floor(p*ns.z*ns.z); + vec4 x_=floor(j*ns.z); + vec4 y_=floor(j-7.0*x_); + vec4 x=x_*ns.x+ns.yyyy; + vec4 y=y_*ns.x+ns.yyyy; + vec4 h=1.0-abs(x)-abs(y); + vec4 b0=vec4(x.xy,y.xy); + vec4 b1=vec4(x.zw,y.zw); + vec4 s0=floor(b0)*2.0+1.0; + vec4 s1=floor(b1)*2.0+1.0; + vec4 sh=-step(h,vec4(0.0)); + vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy; + vec4 a1=b1.xzyw+s1.xzyw*sh.zzww; + vec3 p0=vec3(a0.xy,h.x); + vec3 p1=vec3(a0.zw,h.y); + vec3 p2=vec3(a1.xy,h.z); + vec3 p3=vec3(a1.zw,h.w); + vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3))); + p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w; + vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0); + m=m*m; + return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3))); + } + + void main(){ + float t = uTime * uSpeed; + vec3 pos = position; + + float n1 = snoise(pos * 1.5 + t * 0.3) * uDisplacement; + float n2 = snoise(pos * 3.0 - t * 0.2) * uDisplacement * 0.35; + float n3 = snoise(pos * 5.0 + t * 0.5) * uDisplacement * 0.15; + float disp = n1 + n2 + n3; + + vDisp = disp; + pos += normal * disp; + vNormal = normalize(normalMatrix * normal); + vViewPos = (modelViewMatrix * vec4(pos, 1.0)).xyz; + + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); + } +`; + +// ── Fragment Shader ────────────────────────────────────────────────── +const fragmentShader = ` + uniform float uTime; + uniform vec3 uColor1; + uniform vec3 uColor2; + uniform float uOpacity; + uniform float uFresnelPower; + varying vec3 vNormal; + varying vec3 vViewPos; + varying float vDisp; + + void main(){ + vec3 viewDir = normalize(-vViewPos); + float fresnel = pow(1.0 - max(dot(viewDir, vNormal), 0.0), uFresnelPower); + + float colorMix = smoothstep(-0.15, 0.15, vDisp); + vec3 color = mix(uColor1, uColor2, colorMix); + + color += fresnel * vec3(0.35, 0.35, 0.55); + + float shimmer = sin(vDisp * 30.0 + uTime * 2.0) * 0.03; + color += shimmer; + + float alpha = mix(uOpacity, min(1.0, uOpacity + 0.3), fresnel); + gl_FragColor = vec4(color, alpha); + } +`; + + +export default function agentVoiceOrb() { + return { + // State (Alpine-reaktiv, OK) + voiceMode: false, + voiceState: 'idle', + transcript: '', + supported: false, + + // Audio (primitives, OK als Alpine props) + _energy: 0, + _targetEnergy: 0, + _currentDisp: 0.04, + _targetDisp: 0.04, + _currentSpeed: 0.4, + _targetSpeed: 0.4, + + // Audio nodes (nicht reaktiv nΓΆtig, aber simpel genug) + _audioCtx: null, + _micAnalyser: null, + _playbackAnalyser: null, + _analyser: null, + _micStream: null, + _micSource: null, + _recognition: null, + _dataArray: null, + _silenceTimeout: null, + _lastSpeechTime: 0, + + initVoice() { + this.supported = 'webkitSpeechRecognition' in window || 'SpeechRecognition' in window; + }, + + toggleVoice() { + this.voiceMode ? this.stopVoice() : this.startVoice(); + }, + + async startVoice() { + this.voiceMode = true; + this.voiceState = 'idle'; + await this.$nextTick(); + + this._initThree(); + this._initAudio(); + + setTimeout(() => this.startListening(), 500); + }, + + stopVoice() { + this.voiceMode = false; + this.voiceState = 'idle'; + this._stopMic(); + this._stopRecognition(); + this._destroyThree(); + if (this._audioCtx) { + this._audioCtx.close().catch(() => {}); + this._audioCtx = null; + this._micAnalyser = null; + this._playbackAnalyser = null; + this._analyser = null; + } + }, + + // ── Three.js (alles in externem State, kein Proxy) ────── + _initThree() { + const container = this.$refs.orbContainer; + if (!container) return; + + const size = 280; + const scene = new Scene(); + const camera = new PerspectiveCamera(45, 1, 0.1, 100); + camera.position.z = 3.2; + + const renderer = new WebGLRenderer({ alpha: true, antialias: true }); + renderer.setSize(size, size); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.setClearColor(0x000000, 0); + container.innerHTML = ''; + container.appendChild(renderer.domElement); + + const geo = new IcosahedronGeometry(1, 64); + + const material = new ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms: { + uTime: { value: 0 }, + uDisplacement: { value: 0.05 }, + uSpeed: { value: 0.4 }, + uColor1: { value: new Color(0.35, 0.45, 0.95) }, + uColor2: { value: new Color(0.55, 0.35, 0.95) }, + uOpacity: { value: 0.85 }, + uFresnelPower: { value: 2.5 }, + }, + transparent: true, + side: DoubleSide, + }); + + const mesh = new Mesh(geo, material); + scene.add(mesh); + + // In WeakMap speichern, nicht in Alpine + setThree(container, { + scene, camera, renderer, material, mesh, geo, + startTime: performance.now(), + color1: new Color(0.35, 0.45, 0.95), + color2: new Color(0.55, 0.35, 0.95), + targetColor1: new Color(0.35, 0.45, 0.95), + targetColor2: new Color(0.55, 0.35, 0.95), + }); + + this._animateThree(container); + }, + + _destroyThree() { + const container = this.$refs.orbContainer; + if (!container) return; + + const s = getThree(container); + if (!s) return; + + s.renderer.dispose(); + s.material.dispose(); + s.geo.dispose(); + s.renderer.domElement.remove(); + clearThree(container); + }, + + _animateThree(container) { + const s = getThree(container); + if (!s || !this.voiceMode) return; + + requestAnimationFrame(() => this._animateThree(container)); + + const now = performance.now(); + const elapsed = (now - s.startTime) / 1000; + const dt = Math.min(1 / 30, 1 / 60); // cap + + // Audio energy + if (this._analyser && this._dataArray) { + this._analyser.getByteFrequencyData(this._dataArray); + let sum = 0; + for (let i = 0; i < this._dataArray.length; i++) sum += this._dataArray[i]; + this._targetEnergy = sum / this._dataArray.length / 255; + } else { + this._targetEnergy = 0; + } + this._energy += (this._targetEnergy - this._energy) * 0.12; + + // State targets + switch (this.voiceState) { + case 'listening': + this._targetDisp = 0.06 + this._energy * 0.25; + this._targetSpeed = 0.6 + this._energy * 0.4; + s.targetColor1 = new Color(0.3, 0.5, 1.0); + s.targetColor2 = new Color(0.4, 0.7, 1.0); + break; + case 'thinking': + this._targetDisp = 0.08 + Math.sin(elapsed * 3) * 0.03; + this._targetSpeed = 1.2; + s.targetColor1 = new Color(0.5, 0.3, 0.9); + s.targetColor2 = new Color(0.7, 0.3, 1.0); + break; + case 'speaking': + this._targetDisp = 0.08 + this._energy * 0.35; + this._targetSpeed = 0.8 + this._energy * 0.5; + s.targetColor1 = new Color(0.35, 0.4, 1.0); + s.targetColor2 = new Color(0.5, 0.55, 1.0); + break; + default: + this._targetDisp = 0.04 + Math.sin(elapsed) * 0.015; + this._targetSpeed = 0.3; + s.targetColor1 = new Color(0.35, 0.45, 0.95); + s.targetColor2 = new Color(0.55, 0.35, 0.95); + } + + // Smooth lerp + const lerpRate = 0.06; + this._currentDisp += (this._targetDisp - this._currentDisp) * 0.1; + this._currentSpeed += (this._targetSpeed - this._currentSpeed) * lerpRate; + s.color1.lerp(s.targetColor1, lerpRate); + s.color2.lerp(s.targetColor2, lerpRate); + + // Update uniforms + const u = s.material.uniforms; + u.uTime.value = elapsed; + u.uDisplacement.value = this._currentDisp; + u.uSpeed.value = this._currentSpeed; + u.uColor1.value.copy(s.color1); + u.uColor2.value.copy(s.color2); + + // Rotation + s.mesh.rotation.y = elapsed * 0.15; + s.mesh.rotation.x = Math.sin(elapsed * 0.1) * 0.1; + + s.renderer.render(s.scene, s.camera); + }, + + // ── Audio ───────────────────────────────────────── + _initAudio() { + this._audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + + this._micAnalyser = this._audioCtx.createAnalyser(); + this._micAnalyser.fftSize = 256; + this._micAnalyser.smoothingTimeConstant = 0.8; + + this._playbackAnalyser = this._audioCtx.createAnalyser(); + this._playbackAnalyser.fftSize = 256; + this._playbackAnalyser.smoothingTimeConstant = 0.8; + this._playbackAnalyser.connect(this._audioCtx.destination); + + this._analyser = this._micAnalyser; + this._dataArray = new Uint8Array(128); + }, + + // ── Speech Recognition ──────────────────────────── + startListening() { + if (!this.voiceMode) return; + + this.voiceState = 'listening'; + this.transcript = ''; + this._lastSpeechTime = Date.now(); + this._analyser = this._micAnalyser; + + navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { + this._micStream = stream; + if (this._audioCtx && this._micAnalyser) { + if (this._micSource) { try { this._micSource.disconnect(); } catch(e) {} } + this._micSource = this._audioCtx.createMediaStreamSource(stream); + this._micSource.connect(this._micAnalyser); + } + }).catch(() => {}); + + const SR = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SR) return; + + this._recognition = new SR(); + this._recognition.lang = 'de-DE'; + this._recognition.interimResults = true; + this._recognition.continuous = true; + this._recognition.maxAlternatives = 1; + + let finalTranscript = ''; + + this._recognition.onresult = (event) => { + this._lastSpeechTime = Date.now(); + let interim = ''; + for (let i = event.resultIndex; i < event.results.length; i++) { + if (event.results[i].isFinal) { + finalTranscript += event.results[i][0].transcript + ' '; + } else { + interim += event.results[i][0].transcript; + } + } + this.transcript = (finalTranscript + interim).trim(); + + clearTimeout(this._silenceTimeout); + this._silenceTimeout = setTimeout(() => { + if (this.voiceState !== 'listening') return; + if (this.transcript.trim()) { + const text = this.transcript.trim(); + this._stopRecognition(); + this._stopMic(); + this._sendVoiceMessage(text); + } + }, 2000); + }; + + this._recognition.onend = () => { + if (this.voiceMode && this.voiceState === 'listening') { + if (this.transcript.trim() && Date.now() - this._lastSpeechTime > 1500) { + this._sendVoiceMessage(this.transcript.trim()); + } else { + setTimeout(() => { + if (this.voiceMode && this.voiceState === 'listening') { + try { this._recognition.start(); } catch(e) {} + } + }, 100); + } + } + }; + + this._recognition.onerror = (event) => { + if (['no-speech', 'aborted'].includes(event.error) && this.voiceMode && this.voiceState === 'listening') { + setTimeout(() => { + if (this.voiceMode && this.voiceState === 'listening') { + try { this._recognition.start(); } catch(e) {} + } + }, 200); + } + }; + + try { this._recognition.start(); } catch(e) {} + }, + + async _sendVoiceMessage(text) { + this.voiceState = 'thinking'; + clearTimeout(this._silenceTimeout); + this._stopMic(); + this._stopRecognition(); + + this.pendingMessage = text; + + try { + // send() mit withAudio=true β†’ liefert Audio im selben Request zurΓΌck + const result = await this.$wire.call('send', text, true); + this.pendingMessage = null; + this.sending = false; + + const conv = this.$wire.conversation; + const last = conv?.length > 0 ? conv[conv.length - 1] : null; + + if (last?.role === 'assistant') { + const msg = last.content; + const shouldEnd = msg.includes('[END]'); + const cleanMsg = msg.replace('[END]', '').trim(); + + if (cleanMsg) { + // Audio aus dem kombinierten Response nutzen (kein zweiter Call) + if (result?.audio) { + this.voiceState = 'speaking'; + this._analyser = this._playbackAnalyser; + await this._playAudio(result.audio); + } else { + await this._speak(cleanMsg); + } + } + + if (shouldEnd) { + this.stopVoice(); + return; + } + } + } catch (e) { + this.pendingMessage = null; + this.sending = false; + } + + // Weiter zuhΓΆren + if (this.voiceMode) { + this.voiceState = 'idle'; + setTimeout(() => { + if (this.voiceMode) this.startListening(); + }, 1000); + } + }, + + async _speak(text) { + this.voiceState = 'speaking'; + this._analyser = this._playbackAnalyser; + + try { + // Race: TTS-API vs. 15s Timeout + const result = await Promise.race([ + this.$wire.call('synthesize', text), + new Promise(resolve => setTimeout(() => resolve(null), 15000)), + ]); + + if (result?.audio) { + await this._playAudio(result.audio); + } else { + // API hat nichts zurΓΌckgegeben oder Timeout β†’ Browser-TTS + await this._browserTTS(text); + } + } catch (e) { + await this._browserTTS(text); + } + }, + + _browserTTS(text) { + return new Promise(resolve => { + const u = new SpeechSynthesisUtterance(text); + u.lang = 'de-DE'; + u.onend = resolve; + u.onerror = resolve; + speechSynthesis.speak(u); + }); + }, + + _playAudio(base64) { + return new Promise(resolve => { + const audio = new Audio('data:audio/mp3;base64,' + base64); + if (this._audioCtx && this._playbackAnalyser) { + try { + const src = this._audioCtx.createMediaElementSource(audio); + src.connect(this._playbackAnalyser); + } catch(e) {} + } + audio.onended = resolve; + audio.onerror = resolve; + audio.play().catch(resolve); + }); + }, + + _stopMic() { + if (this._micSource) { try { this._micSource.disconnect(); } catch(e) {} this._micSource = null; } + if (this._micStream) { this._micStream.getTracks().forEach(t => t.stop()); this._micStream = null; } + }, + + _stopRecognition() { + clearTimeout(this._silenceTimeout); + if (this._recognition) { try { this._recognition.abort(); } catch(e) {} this._recognition = null; } + }, + }; +} diff --git a/src/resources/js/app.js b/src/resources/js/app.js new file mode 100644 index 0000000..1bdefd6 --- /dev/null +++ b/src/resources/js/app.js @@ -0,0 +1,47 @@ +import './bootstrap'; +// import './calendar-interact.js'; +import './calendar-week-canvas.js'; +import agentVoiceOrb from './agent-voice-orb.js'; + +window.agentVoiceOrb = agentVoiceOrb; + +import '../plugins/Notification/index.js'; +import './websocket/connection.js'; + +document.addEventListener('livewire:init', () => { + + Livewire.on('notify', (payload) => { + + const data = Array.isArray(payload) ? payload[0] : payload; + + message.toast({ + type: data.type ?? 'info', + title: data.title ?? '', + text: data.message ?? '', + duration: data.duration ?? undefined, + + onClick: () => { + if (data.action?.event) { + Livewire.dispatch(data.action.event); + } + } + }); + + }); + +}); + + +function openCenteredWindow(url, title, w, h) { + // Calculate position + const left = (screen.width / 2) - (w / 2); + const top = (screen.height / 2) - (h / 2); + + return window.open( + url, + title, + `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, width=${w}, height=${h}, top=${top}, left=${left}` + ); +} + +window.openCenteredWindow = openCenteredWindow; diff --git a/src/resources/js/bootstrap.js b/src/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/src/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/src/resources/js/calendar-interact.js b/src/resources/js/calendar-interact.js new file mode 100644 index 0000000..d05e965 --- /dev/null +++ b/src/resources/js/calendar-interact.js @@ -0,0 +1,1432 @@ +// import interact from 'interactjs'; +// +// export default function initCalendar() { +// +// +// interact('.calendar-event') +// +// /* +// |-------------------------------------------------------------------------- +// | DRAG (NUR EIN ELEMENT) +// |-------------------------------------------------------------------------- +// */ +// .draggable({ +// inertia: false, +// +// listeners: { +// +// start(event) { +// const t = event.target; +// t.style.transition = 'none'; +// +// const rect = t.getBoundingClientRect(); +// +// const ghost = t.cloneNode(true); +// +// ghost.style.position = 'fixed'; +// ghost.style.left = rect.left + 'px'; +// ghost.style.top = rect.top + 'px'; +// ghost.style.width = rect.width + 'px'; +// ghost.style.height = rect.height + 'px'; +// +// ghost.style.opacity = '0.25'; +// ghost.style.pointerEvents = 'none'; +// ghost.style.zIndex = 998; +// +// document.body.appendChild(ghost); +// +// t._ghost = ghost; +// +// t.style.zIndex = 999; +// }, +// +// move(event) { +// +// const t = event.target; +// +// let x = (parseFloat(t.dataset.x) || 0) + event.dx; +// let y = (parseFloat(t.dataset.y) || 0) + event.dy; +// +// t.style.transform = `translate(${x}px, ${y}px)`; +// +// t.dataset.x = x; +// t.dataset.y = y; +// +// let column = t.closest('[data-day-column]'); +// if (!column) return; +// +// let result = calculateTime(t, column); +// if (!result) return; +// +// let timeEl = t.querySelector('.event-time'); +// +// if (timeEl) { +// timeEl.innerText = `${result.start} – ${result.end}`; +// } +// +// }, +// +// end(event) { +// +// const t = event.target; +// +// let column = getColumnFromPointer(event); +// if (!column) return; +// +// let grid = column.querySelector('.week-grid'); +// let gridRect = grid.getBoundingClientRect(); +// let elRect = t.getBoundingClientRect(); +// +// let hourHeight = grid.offsetHeight / 24; +// +// /* +// |-------------------------------------------------------------------------- +// | POSITION +// |-------------------------------------------------------------------------- +// */ +// let top = elRect.top - gridRect.top; +// +// let minutes = Math.round(((top / hourHeight) * 60) / 15) * 15; +// minutes = clamp(minutes, 0, 1439); +// +// let snappedTop = (minutes / 60) * hourHeight; +// +// t.style.top = snappedTop + 'px'; +// grid.appendChild(t); +// +// /* +// |-------------------------------------------------------------------------- +// | ZEIT +// |-------------------------------------------------------------------------- +// */ +// let duration = (elRect.height / hourHeight) * 60; +// +// let endMinutes = Math.round((minutes + duration) / 15) * 15; +// endMinutes = clamp(endMinutes, 0, 1439); +// +// let startTime = formatTime(minutes); +// let endTime = formatTime(endMinutes); +// +// let newDate = column.dataset.date; +// +// /* +// |-------------------------------------------------------------------------- +// | SAVE +// |-------------------------------------------------------------------------- +// */ +// let component = t.closest('[wire\\:id]'); +// +// if (component) { +// Livewire.find(component.getAttribute('wire:id')) +// .call('updateEventTimeAndDate', +// t.dataset.id, +// newDate, +// startTime, +// endTime +// ); +// } +// +// /* +// |-------------------------------------------------------------------------- +// | RESET +// |-------------------------------------------------------------------------- +// */ +// t.style.transform = 'none'; +// t.dataset.x = 0; +// t.dataset.y = 0; +// +// +// if (t._ghost) { +// t._ghost.remove(); +// t._ghost = null; +// } +// +// t.style.zIndex = ''; +// +// } +// } +// }) +// +// /* +// |-------------------------------------------------------------------------- +// | RESIZE (NUR Y!) +// |-------------------------------------------------------------------------- +// */ +// .resizable({ +// edges: { +// top: '.resize-top', +// bottom: '.resize-bottom' +// }, +// +// inertia: false, +// +// listeners: { +// +// start(event) { +// const t = event.target; +// t.style.transition = 'none'; +// }, +// +// move(event) { +// +// const t = event.target; +// +// let y = parseFloat(t.dataset.y) || 0; +// +// let height = event.rect.height; +// height = Math.max(height, 26); +// +// t.style.height = height + 'px'; +// +// if (event.edges.top) { +// y += event.deltaRect.top; +// t.style.transform = `translateY(${y}px)`; +// t.dataset.y = y; +// } +// +// let column = t.closest('[data-day-column]'); +// if (!column) return; +// +// let result = calculateTime(t, column); +// if (!result) return; +// +// let timeEl = t.querySelector('.event-time'); +// +// if (timeEl) { +// timeEl.innerText = `${result.start} – ${result.end}`; +// } +// +// }, +// +// end(event) { +// +// const t = event.target; +// +// const container = document.querySelector('[data-allday-row]'); +// if (!container) return; +// +// const rect = container.getBoundingClientRect(); +// const colWidth = rect.width / 7; +// +// let deltaX = event.clientX - t.dataset.startX; +// let shiftDays = Math.round(deltaX / colWidth); +// +// const startDate = t.dataset.start; +// if (!startDate) { +// reset(t); +// return; +// } +// +// const newDate = addDays(startDate, shiftDays); +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ UI DIREKT FINAL SETZEN (WICHTIG!) +// |-------------------------------------------------------------------------- +// */ +// let dayIndex = Math.floor((new Date(newDate) - new Date(container.dataset.weekStart)) / (1000 * 60 * 60 * 24)); +// +// dayIndex = Math.max(0, Math.min(6, dayIndex)); +// +// t.style.transform = 'none'; +// t.style.left = `calc(${(dayIndex / 7) * 100}% + 4px)`; +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ START UPDATEN (SONST SPRINGT ER BEIM NΓ„CHSTEN DRAG) +// |-------------------------------------------------------------------------- +// */ +// t.dataset.start = newDate; +// +// /* +// |-------------------------------------------------------------------------- +// | SAVE +// |-------------------------------------------------------------------------- +// */ +// const component = t.closest('[wire\\:id]'); +// +// if (component) { +// Livewire.find(component.getAttribute('wire:id')) +// .call('moveEventToDate', t.dataset.id, newDate); +// } +// +// reset(t); +// } +// +// } +// }); +// } +// +// +// +// function addDays(dateString, days) { +// if (!dateString) return null; +// +// const date = new Date(dateString + 'T00:00:00'); +// if (isNaN(date)) return null; +// +// date.setDate(date.getDate() + days); +// +// return date.toISOString().split('T')[0]; +// } +// +// export function initAllDayDrag() { +// +// interact('.calendar-allday-event').draggable({ +// inertia: false, +// +// listeners: { +// +// start(event) { +// const t = event.target; +// +// t.style.transition = 'none'; +// +// const rect = t.getBoundingClientRect(); +// +// /* +// |-------------------------------------------------------------------------- +// | GHOST (gleich wie grid) +// |-------------------------------------------------------------------------- +// */ +// const ghost = t.cloneNode(true); +// +// ghost.style.position = 'fixed'; +// ghost.style.left = rect.left + 'px'; +// ghost.style.top = rect.top + 'px'; +// ghost.style.width = rect.width + 'px'; +// ghost.style.height = rect.height + 'px'; +// +// ghost.style.opacity = '0.25'; +// ghost.style.pointerEvents = 'none'; +// ghost.style.zIndex = 998; +// +// document.body.appendChild(ghost); +// +// t._ghost = ghost; +// +// t.style.zIndex = 999; +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ INIT +// |-------------------------------------------------------------------------- +// */ +// t.dataset.x = 0; +// t.dataset.startCol = parseInt(t.dataset.col); +// t.dataset.startDate = t.dataset.start; +// }, +// +// move(event) { +// +// const t = event.target; +// +// let x = (parseFloat(t.dataset.x) || 0) + event.dx; +// t.dataset.x = x; +// +// t.style.transform = `translateX(${x}px)`; +// +// const container = t.closest('[data-allday-row]'); +// const rect = container.getBoundingClientRect(); +// const colWidth = rect.width / 7; +// +// let movedCols = x / colWidth; +// +// let previewCol = Math.round( +// parseInt(t.dataset.startCol) + movedCols +// ); +// +// let span = parseInt(t.dataset.span || 1); +// let maxStart = 7 - span; +// +// if (previewCol < 0) previewCol = 0; +// if (previewCol > maxStart) previewCol = maxStart; +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ VISUAL POSITION +// |-------------------------------------------------------------------------- +// */ +// t.style.left = `calc(${previewCol} * (100% / 7))`; +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ COLUMN HIGHLIGHT +// |-------------------------------------------------------------------------- +// */ +// document.querySelectorAll('[data-day-column]').forEach(el => { +// el.classList.remove('allday-drop-preview'); +// }); +// +// let cols = document.querySelectorAll('[data-day-column]'); +// let targetCol = cols[previewCol]; +// +// if (targetCol) { +// targetCol.classList.add('allday-drop-preview'); +// } +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ DROP LINE (optional, aber geil) +// |-------------------------------------------------------------------------- +// */ +// let line = document.getElementById('allday-drop-line'); +// +// if (line) { +// line.classList.remove('hidden'); +// +// line.style.left = (previewCol * colWidth + rect.left) + 'px'; +// } +// }, +// +// end(event) { +// +// const t = event.target; +// +// const container = t.closest('[data-allday-row]'); +// const rect = container.getBoundingClientRect(); +// const colWidth = rect.width / 7; +// +// let x = parseFloat(t.dataset.x || 0); +// +// /* +// |-------------------------------------------------------------------------- +// | πŸ”₯ FINAL SHIFT (STABIL) +// |-------------------------------------------------------------------------- +// */ +// let movedCols = Math.round(x / colWidth); +// +// let startCol = parseInt(t.dataset.startCol); +// let span = parseInt(t.dataset.span || 1); +// +// let finalCol = startCol + movedCols; +// +// let maxStart = 7 - span; +// +// if (finalCol < 0) finalCol = 0; +// if (finalCol > maxStart) finalCol = maxStart; +// +// /* +// |-------------------------------------------------------------------------- +// | DATE +// |-------------------------------------------------------------------------- +// */ +// let newDate = addDays(t.dataset.startDate, movedCols); +// +// if (!newDate) { +// cleanup(t); +// return; +// } +// +// /* +// |-------------------------------------------------------------------------- +// | UI FIX +// |-------------------------------------------------------------------------- +// */ +// t.style.transform = 'none'; +// t.style.left = `calc(${finalCol} * (100% / 7))`; +// +// t.dataset.x = 0; +// t.dataset.col = finalCol; +// +// /* +// |-------------------------------------------------------------------------- +// | SAVE +// |-------------------------------------------------------------------------- +// */ +// let component = t.closest('[wire\\:id]'); +// +// if (component) { +// Livewire.find(component.getAttribute('wire:id')) +// .call('moveEventToDate', t.dataset.id, newDate); +// } +// +// cleanup(t); +// } +// } +// }); +// +// function cleanup(t) { +// if (t._ghost) { +// t._ghost.remove(); +// t._ghost = null; +// } +// +// t.style.zIndex = ''; +// } +// } +// +// function reset(t) { +// t.style.transform = ''; +// } +// +// +// +// /* +// |-------------------------------------------------------------------------- +// | HELPERS +// |-------------------------------------------------------------------------- +// */ +// +// +// // function addDays(date, days) { +// // let d = new Date(date); +// // d.setDate(d.getDate() + days); +// // return d.toISOString().slice(0, 10); +// // } +// +// function getColumnFromPointer(event) { +// let found = null; +// +// document.querySelectorAll('[data-day-column]').forEach(col => { +// let r = col.getBoundingClientRect(); +// +// if (event.clientX >= r.left && event.clientX <= r.right) { +// found = col; +// } +// }); +// +// return found; +// } +// +// function resetAllDay(t) { +// +// t.style.transform = ''; +// t.dataset.x = 0; +// +// const ghostId = t.dataset.ghostId; +// +// if (ghostId) { +// document +// .querySelectorAll(`[data-ghost-id="${ghostId}"]`) +// .forEach(el => el.remove()); +// +// delete t.dataset.ghostId; +// } +// +// t.style.zIndex = ''; +// } +// +// function calculateTime(target, column) { +// +// let grid = column.querySelector('.week-grid'); +// +// let gridRect = grid.getBoundingClientRect(); +// let elRect = target.getBoundingClientRect(); +// +// let hourHeight = grid.offsetHeight / 24; +// +// let top = elRect.top - gridRect.top; +// let height = elRect.height; +// +// let startMin = clamp(Math.round(((top / hourHeight) * 60) / 15) * 15, 0, 1439); +// let endMin = clamp(Math.round(((top + height) / hourHeight * 60) / 15) * 15, 0, 1439); +// +// return { +// start: formatTime(startMin), +// end: formatTime(endMin) +// }; +// } +// +// function formatTime(min) { +// let h = Math.floor(min / 60); +// let m = min % 60; +// return `${pad(h)}:${pad(m)}`; +// } +// +// function clamp(v, min, max) { +// return Math.max(min, Math.min(max, v)); +// } +// +// function resetTransform(t) { +// t.style.transform = ''; +// t.dataset.x = 0; +// t.dataset.y = 0; +// } +// +// function pad(n) { +// return String(n).padStart(2, '0'); +// } +// +// /* +// |-------------------------------------------------------------------------- +// | INIT +// |-------------------------------------------------------------------------- +// */ +// document.addEventListener('DOMContentLoaded', () => { +// initCalendar(); +// initAllDayDrag(); +// }); +// +// +// +// // import interact from 'interactjs'; +// // +// // +// // export default function initCalendar() { +// // +// // interact('.calendar-event') +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | DRAG +// // |-------------------------------------------------------------------------- +// // */ +// // .draggable({ +// // inertia: false, +// // +// // listeners: { +// // +// // start(event) { +// // event.target.classList.add('dragging'); +// // event.target.style.transition = 'none'; +// // }, +// // +// // move(event) { +// // +// // let target = event.target; +// // +// // let x = (parseFloat(target.dataset.x) || 0) + event.dx; +// // let y = (parseFloat(target.dataset.y) || 0) + event.dy; +// // +// // // βœ… WICHTIG: transform wieder aktiv +// // // target.style.transform = `translate(${x}px, ${y}px)`; +// // +// // let group = getGroup(target); +// // +// // group.forEach(el => { +// // el.style.transform = `translate(${x}px, ${y}px)`; +// // el.dataset.x = x; +// // el.dataset.y = y; +// // }); +// // +// // target.dataset.x = x; +// // target.dataset.y = y; +// // +// // // βœ… SNAP nur fΓΌr Anzeige +// // updatePreview(target); +// // }, +// // +// // end(event) { +// // +// // let target = event.target; +// // +// // let column = getColumnFromPointer(event); +// // if (!column) return; +// // +// // let grid = column.querySelector('.week-grid'); +// // let gridRect = grid.getBoundingClientRect(); +// // let elRect = target.getBoundingClientRect(); +// // +// // let hourHeight = grid.offsetHeight / 24; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | πŸ”₯ POSITION FINAL (SOFORT SETZEN) +// // |-------------------------------------------------------------------------- +// // */ +// // let top = elRect.top - gridRect.top; +// // +// // let startMinutes = (top / hourHeight) * 60; +// // startMinutes = Math.round(startMinutes / 15) * 15; +// // +// // let snappedTop = (startMinutes / 60) * hourHeight; +// // +// // // πŸ‘‰ WICHTIG: direkt final setzen +// // // target.style.top = snappedTop + 'px'; +// // let group = getGroup(target); +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | πŸ”₯ X β†’ SPALTE (SOFORT!) +// // |-------------------------------------------------------------------------- +// // */ +// // let newDate = column.dataset.date; +// // +// // // πŸ‘‰ MOVE DOM direkt in neue Spalte +// // // column.querySelector('.week-grid').appendChild(target); +// // group.forEach(el => { +// // +// // let elRect = el.getBoundingClientRect(); +// // let top = elRect.top - gridRect.top; +// // +// // let startMinutes = (top / hourHeight) * 60; +// // startMinutes = Math.round(startMinutes / 15) * 15; +// // +// // let snappedTop = (startMinutes / 60) * hourHeight; +// // +// // el.style.top = snappedTop + 'px'; +// // +// // column.querySelector('.week-grid').appendChild(el); +// // +// // el.style.transform = 'none'; +// // el.dataset.x = 0; +// // el.dataset.y = 0; +// // }); +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | ZEIT +// // |-------------------------------------------------------------------------- +// // */ +// // let duration = (elRect.height / hourHeight) * 60; +// // +// // let endMinutes = startMinutes + duration; +// // endMinutes = Math.round(endMinutes / 15) * 15; +// // +// // let startHour = Math.floor(startMinutes / 60); +// // let startMin = startMinutes % 60; +// // +// // let endHour = Math.floor(endMinutes / 60); +// // let endMin = endMinutes % 60; +// // +// // let startTime = `${pad(startHour)}:${pad(startMin)}`; +// // let endTime = `${pad(endHour)}:${pad(endMin)}`; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | SAVE (JETZT IST DOM SCHON RICHTIG!) +// // |-------------------------------------------------------------------------- +// // */ +// // let component = target.closest('[wire\\:id]'); +// // +// // if (component) { +// // Livewire.find(component.getAttribute('wire:id')) +// // .call('updateEventTimeAndDate', +// // target.dataset.id, +// // newDate, +// // startTime, +// // endTime +// // ); +// // } +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | RESET +// // |-------------------------------------------------------------------------- +// // */ +// // target.style.transform = 'none'; +// // target.dataset.x = 0; +// // target.dataset.y = 0; +// // } +// // } +// // }) +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | RESIZE +// // |-------------------------------------------------------------------------- +// // */ +// // .resizable({ +// // edges: { +// // top: '.resize-top', +// // bottom: '.resize-bottom' +// // }, +// // +// // inertia: false, +// // +// // listeners: { +// // +// // move(event) { +// // +// // let target = event.target; +// // +// // let y = parseFloat(target.dataset.y) || 0; +// // +// // // βœ… HΓΆhe setzen +// // target.style.height = event.rect.height + 'px'; +// // +// // // βœ… TOP EDGE korrekt behandeln +// // if (event.edges.top) { +// // y += event.deltaRect.top; +// // target.style.transform = `translateY(${y}px)`; +// // target.dataset.y = y; +// // } +// // +// // updatePreview(target); +// // }, +// // +// // end(event) { +// // +// // let target = event.target; +// // +// // let column = getColumnFromPointer(event); +// // if (!column) return; +// // +// // let result = calculateTime(target, column); +// // if (!result) return; +// // +// // let component = target.closest('[wire\\:id]'); +// // +// // if (component) { +// // Livewire.find(component.getAttribute('wire:id')) +// // .call( +// // 'updateEventTimeAndDate', +// // target.dataset.id, +// // column.dataset.date, +// // result.start, +// // result.end +// // ); +// // } +// // +// // resetTransform(target); +// // } +// // } +// // }); +// // } +// // +// // function getGroup(target) { +// // let id = target.dataset.eventGroup; +// // return document.querySelectorAll(`[data-event-group="${id}"]`); +// // } +// // +// // function getColumnFromPointer(event) { +// // let found = null; +// // +// // document.querySelectorAll('[data-day-column]').forEach(col => { +// // let r = col.getBoundingClientRect(); +// // +// // if ( +// // event.clientX >= r.left && +// // event.clientX <= r.right +// // ) { +// // found = col; +// // } +// // }); +// // +// // return found; +// // } +// // +// // function calculateTime(target, column) { +// // +// // let grid = column.querySelector('.week-grid'); +// // +// // let gridRect = grid.getBoundingClientRect(); +// // let elRect = target.getBoundingClientRect(); +// // +// // let hourHeight = grid.offsetHeight / 24; +// // +// // let top = elRect.top - gridRect.top; +// // let height = elRect.height; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | πŸ”₯ SNAP AUF 15 MINUTEN (GENAU WIE END!) +// // |-------------------------------------------------------------------------- +// // */ +// // let startMinutes = (top / hourHeight) * 60; +// // startMinutes = Math.round(startMinutes / 15) * 15; +// // +// // let duration = (height / hourHeight) * 60; +// // let endMinutes = startMinutes + duration; +// // endMinutes = Math.round(endMinutes / 15) * 15; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | ZEIT BERECHNEN +// // |-------------------------------------------------------------------------- +// // */ +// // let startHour = Math.floor(startMinutes / 60); +// // let startMin = startMinutes % 60; +// // +// // let endHour = Math.floor(endMinutes / 60); +// // let endMin = endMinutes % 60; +// // +// // return { +// // start: `${pad(startHour)}:${pad(startMin)}`, +// // end: `${pad(endHour)}:${pad(endMin)}` +// // }; +// // } +// // +// // function updatePreview(target) { +// // +// // let column = target.closest('[data-day-column]'); +// // if (!column) return; +// // +// // let result = calculateTime(target, column); +// // if (!result) return; +// // +// // let preview = target.querySelector('.time-preview'); +// // +// // if (!preview) { +// // preview = document.createElement('div'); +// // preview.className = 'time-preview absolute bottom-1 right-0 text-[10px] bg-primary-500/70 px-2 py-1 rounded-lg'; +// // target.appendChild(preview); +// // } +// // +// // preview.innerText = `${result.start} - ${result.end}`; +// // } +// // +// // function resetTransform(target) { +// // target.style.transform = ''; +// // target.dataset.x = 0; +// // target.dataset.y = 0; +// // } +// // +// // function pad(n) { +// // return String(n).padStart(2, '0'); +// // } +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | INIT +// // |-------------------------------------------------------------------------- +// // */ +// // document.addEventListener('DOMContentLoaded', () => { +// // initCalendar(); +// // }); +// +// // export default function initCalendar() { +// // +// // interact('.calendar-event') +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | DRAG (SMOOTH + PREVIEW) +// // |-------------------------------------------------------------------------- +// // */ +// // .draggable({ +// // inertia: false, +// // +// // listeners: { +// // +// // start(event) { +// // event.target.classList.add('dragging'); +// // }, +// // +// // move(event) { +// // +// // let target = event.target; +// // +// // let x = (parseFloat(target.dataset.x) || 0) + event.dx; +// // let y = (parseFloat(target.dataset.y) || 0) + event.dy; +// // +// // // πŸ”₯ KEIN SNAP HIER! +// // // target.style.transform = `translate(${x}px, ${y}px)`; +// // +// // target.dataset.x = x; +// // target.dataset.y = y; +// // +// // updatePreview(target); +// // }, +// // +// // end(event) { +// // +// // let target = event.target; +// // +// // let column = getColumnFromPointer(event); +// // if (!column) return; +// // +// // let grid = column.querySelector('.week-grid'); +// // let gridRect = grid.getBoundingClientRect(); +// // let elRect = target.getBoundingClientRect(); +// // +// // let hourHeight = grid.offsetHeight / 24; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | πŸ”₯ POSITION β†’ DIREKT SNAP (KEIN RAW TOP!) +// // |-------------------------------------------------------------------------- +// // */ +// // let rawTop = elRect.top - gridRect.top; +// // +// // let startMinutes = (rawTop / hourHeight) * 60; +// // startMinutes = Math.round(startMinutes / 15) * 15; +// // +// // let snappedTop = (startMinutes / 60) * hourHeight; +// // +// // // πŸ‘‰ WICHTIG: direkt richtige Position setzen +// // target.style.top = snappedTop + 'px'; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | DAUER (BLEIBT WIE IST) +// // |-------------------------------------------------------------------------- +// // */ +// // let duration = (elRect.height / hourHeight) * 60; +// // +// // let endMinutes = startMinutes + duration; +// // endMinutes = Math.round(endMinutes / 15) * 15; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | ZEIT FORMAT +// // |-------------------------------------------------------------------------- +// // */ +// // let startHour = Math.floor(startMinutes / 60); +// // let startMin = startMinutes % 60; +// // +// // let endHour = Math.floor(endMinutes / 60); +// // let endMin = endMinutes % 60; +// // +// // let startTime = `${pad(startHour)}:${pad(startMin)}`; +// // let endTime = `${pad(endHour)}:${pad(endMin)}`; +// // +// // let newDate = column.dataset.date; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | SAVE +// // |-------------------------------------------------------------------------- +// // */ +// // let component = target.closest('[wire\\:id]'); +// // +// // if (component) { +// // Livewire.find(component.getAttribute('wire:id')) +// // .call('updateEventTimeAndDate', +// // target.dataset.id, +// // newDate, +// // startTime, +// // endTime +// // ) +// // .then(() => { +// // Livewire.find(component.getAttribute('wire:id')).call('$refresh'); +// // }); +// // } +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | RESET (NUR TRANSFORM!) +// // |-------------------------------------------------------------------------- +// // */ +// // // target.style.transform = ''; +// // target.dataset.x = 0; +// // target.dataset.y = 0; +// // } +// // } +// // }) +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | RESIZE (SMOOTH) +// // |-------------------------------------------------------------------------- +// // */ +// // .resizable({ +// // edges: { +// // top: '.resize-top', +// // bottom: '.resize-bottom' +// // }, +// // +// // inertia: false, +// // +// // listeners: { +// // +// // move(event) { +// // +// // let target = event.target; +// // +// // let y = parseFloat(target.dataset.y) || 0; +// // +// // target.style.height = event.rect.height + 'px'; +// // +// // // if (event.edges.top) { +// // y += event.deltaRect.top; +// // // target.style.transform = `translateY(${y}px)`; +// // target.dataset.y = y; +// // // } +// // +// // updatePreview(target); +// // }, +// // +// // end(event) { +// // +// // let target = event.target; +// // +// // let column = getColumnFromPointer(event); +// // if (!column) return; +// // +// // let result = calculateTime(target, column); +// // if (!result) return; +// // +// // let component = target.closest('[wire\\:id]'); +// // +// // if (component) { +// // Livewire.find(component.getAttribute('wire:id')) +// // .call( +// // 'updateEventTimeAndDate', +// // target.dataset.id, +// // column.dataset.date, +// // result.start, +// // result.end +// // ) +// // .then(() => { +// // // πŸ”₯ ERST NACH LIVEWIRE RESETTEN +// // resetTransform(target); +// // }); +// // } else { +// // resetTransform(target); +// // } +// // } +// // } +// // }); +// // } +// +// /* +// |-------------------------------------------------------------------------- +// | HELPERS +// |-------------------------------------------------------------------------- +// */ +// +// +// +// // import interact from 'interactjs'; +// // +// // export default function initCalendar() { +// // +// // interact('.calendar-event') +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | DRAG +// // |-------------------------------------------------------------------------- +// // */ +// // .draggable({ +// // inertia: true, +// // +// // listeners: { +// // move(event) { +// // +// // let target = event.target; +// // +// // let x = (parseFloat(target.dataset.x) || 0) + event.dx; +// // let y = (parseFloat(target.dataset.y) || 0) + event.dy; +// // +// // target.style.transform = `translate(${x}px, ${y}px)`; +// // +// // target.dataset.x = x; +// // target.dataset.y = y; +// // }, +// // +// // end(event) { +// // +// // let target = event.target; +// // +// // // πŸ”₯ Column bestimmen (X-Achse) +// // let column = null; +// // +// // document.querySelectorAll('[data-day-column]').forEach(col => { +// // let r = col.getBoundingClientRect(); +// // +// // if ( +// // event.clientX >= r.left && +// // event.clientX <= r.right +// // ) { +// // column = col; +// // } +// // }); +// // +// // if (!column) return; +// // +// // let grid = column.querySelector('.week-grid'); +// // +// // let gridRect = grid.getBoundingClientRect(); +// // let elRect = target.getBoundingClientRect(); +// // +// // let hourHeight = grid.offsetHeight / 24; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | POSITION (EXAKT - KEIN SPRINGEN) +// // |-------------------------------------------------------------------------- +// // */ +// // let top = elRect.top - gridRect.top; +// // +// // let startMinutes = (top / hourHeight) * 60; +// // +// // let startHour = Math.floor(startMinutes / 60); +// // let startMin = Math.round(startMinutes % 60); +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | DAUER (WICHTIG!) +// // |-------------------------------------------------------------------------- +// // */ +// // let duration = (elRect.height / hourHeight) * 60; +// // +// // let endMinutes = startMinutes + duration; +// // +// // let endHour = Math.floor(endMinutes / 60); +// // let endMin = Math.round(endMinutes % 60); +// // +// // let startTime = `${String(startHour).padStart(2,'0')}:${String(startMin).padStart(2,'0')}`; +// // let endTime = `${String(endHour).padStart(2,'0')}:${String(endMin).padStart(2,'0')}`; +// // +// // let newDate = column.dataset.date; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | SAVE +// // |-------------------------------------------------------------------------- +// // */ +// // let component = target.closest('[wire\\:id]'); +// // +// // if (component) { +// // Livewire.find(component.getAttribute('wire:id')) +// // .call('updateEventTimeAndDate', target.dataset.id, newDate, startTime, endTime); +// // } +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | RESET TRANSFORM +// // |-------------------------------------------------------------------------- +// // */ +// // target.style.transform = ''; +// // target.dataset.x = 0; +// // target.dataset.y = 0; +// // } +// // } +// // }) +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | RESIZE +// // |-------------------------------------------------------------------------- +// // */ +// // .resizable({ +// // edges: { +// // top: '.resize-top', +// // bottom: '.resize-bottom' +// // }, +// // +// // inertia: false, +// // +// // listeners: { +// // +// // move(event) { +// // +// // let target = event.target; +// // +// // let y = parseFloat(target.dataset.y) || 0; +// // +// // // HΓΆhe direkt setzen (kein Snap!) +// // target.style.height = event.rect.height + 'px'; +// // +// // // Wenn oben resized β†’ verschieben +// // if (event.edges.top) { +// // y += event.deltaRect.top; +// // +// // target.style.transform = `translateY(${y}px)`; +// // target.dataset.y = y; +// // } +// // }, +// // +// // end(event) { +// // +// // let target = event.target; +// // +// // let column = target.closest('[data-day-column]'); +// // if (!column) return; +// // +// // let grid = column.querySelector('.week-grid'); +// // +// // let gridRect = grid.getBoundingClientRect(); +// // let elRect = target.getBoundingClientRect(); +// // +// // let hourHeight = grid.offsetHeight / 24; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | POSITION + HEIGHT (GLEICH WIE DRAG!) +// // |-------------------------------------------------------------------------- +// // */ +// // let top = elRect.top - gridRect.top; +// // let height = elRect.height; +// // +// // let startMinutes = (top / hourHeight) * 60; +// // let endMinutes = ((top + height) / hourHeight) * 60; +// // +// // let startHour = Math.floor(startMinutes / 60); +// // let startMin = Math.round(startMinutes % 60); +// // +// // let endHour = Math.floor(endMinutes / 60); +// // let endMin = Math.round(endMinutes % 60); +// // +// // let startTime = `${String(startHour).padStart(2,'0')}:${String(startMin).padStart(2,'0')}`; +// // let endTime = `${String(endHour).padStart(2,'0')}:${String(endMin).padStart(2,'0')}`; +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | SAVE +// // |-------------------------------------------------------------------------- +// // */ +// // let component = target.closest('[wire\\:id]'); +// // +// // if (component) { +// // Livewire.find(component.getAttribute('wire:id')) +// // .call('updateEventTimeAndDate', target.dataset.id, column.dataset.date, startTime, endTime); +// // } +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | RESET +// // |-------------------------------------------------------------------------- +// // */ +// // target.style.transform = ''; +// // target.dataset.y = 0; +// // } +// // } +// // }); +// // } +// // +// // /* +// // |-------------------------------------------------------------------------- +// // | INIT +// // |-------------------------------------------------------------------------- +// // */ +// // document.addEventListener('DOMContentLoaded', () => { +// // initCalendar(); +// // }); +// // +// // +// // +// // // import interact from 'interactjs'; +// // // +// // // export default function initCalendar() { +// // // +// // // interact('.calendar-event') +// // // +// // // /* +// // // |-------------------------------------------------------------------------- +// // // | DRAG +// // // |-------------------------------------------------------------------------- +// // // */ +// // // .draggable({ +// // // listeners: { +// // // move(event) { +// // // +// // // let target = event.target; +// // // +// // // let x = (parseFloat(target.dataset.x) || 0) + event.dx; +// // // let y = (parseFloat(target.dataset.y) || 0) + event.dy; +// // // +// // // target.style.transform = `translate(${x}px, ${y}px)`; +// // // +// // // target.dataset.x = x; +// // // target.dataset.y = y; +// // // }, +// // // +// // // end(event) { +// // // +// // // let target = event.target; +// // // +// // // let column = null; +// // // +// // // document.querySelectorAll('[data-day-column]').forEach(col => { +// // // let r = col.getBoundingClientRect(); +// // // +// // // if ( +// // // event.clientX >= r.left && +// // // event.clientX <= r.right +// // // ) { +// // // column = col; +// // // } +// // // }); +// // // +// // // if (!column) return; +// // // +// // // let grid = column.querySelector('.week-grid'); +// // // let hourHeight = grid.offsetHeight / 24; +// // // +// // // // πŸ”₯ FIX (kein Springen mehr) +// // // let offsetY = parseFloat(target.dataset.y) || 0; +// // // let top = target.offsetTop + offsetY; +// // // +// // // let startMinutes = (top / hourHeight) * 60; +// // // +// // // let startHour = Math.floor(startMinutes / 60); +// // // let startMin = Math.round(startMinutes % 60); +// // // +// // // let startTime = `${String(startHour).padStart(2,'0')}:${String(startMin).padStart(2,'0')}`; +// // // let endTime = `${String(startHour + 1).padStart(2,'0')}:${String(startMin).padStart(2,'0')}`; +// // // +// // // let newDate = column.dataset.date; +// // // +// // // let component = target.closest('[wire\\:id]'); +// // // +// // // if (component) { +// // // Livewire.find(component.getAttribute('wire:id')) +// // // .call('updateEventTimeAndDate', target.dataset.id, newDate, startTime, endTime); +// // // } +// // // +// // // // RESET (bleibt!) +// // // target.style.transform = ''; +// // // target.dataset.x = 0; +// // // target.dataset.y = 0; +// // // } +// // // } +// // // }) +// // // +// // // /* +// // // |-------------------------------------------------------------------------- +// // // | RESIZE +// // // |-------------------------------------------------------------------------- +// // // */ +// // // .resizable({ +// // // edges: { +// // // top: '.resize-top', +// // // bottom: '.resize-bottom' +// // // }, +// // // +// // // listeners: { +// // // move(event) { +// // // +// // // let target = event.target; +// // // +// // // let y = parseFloat(target.dataset.y) || 0; +// // // +// // // target.style.height = event.rect.height + 'px'; +// // // +// // // if (event.edges.top) { +// // // y += event.deltaRect.top; +// // // target.style.transform = `translateY(${y}px)`; +// // // target.dataset.y = y; +// // // } +// // // }, +// // // +// // // end(event) { +// // // +// // // let target = event.target; +// // // +// // // let column = target.closest('[data-day-column]'); +// // // let grid = column.querySelector('.week-grid'); +// // // +// // // let rect = grid.getBoundingClientRect(); +// // // let elRect = target.getBoundingClientRect(); +// // // +// // // let hourHeight = grid.offsetHeight / 24; +// // // +// // // let top = elRect.top - rect.top; +// // // let height = elRect.height; +// // // +// // // let startMinutes = (top / hourHeight) * 60; +// // // let endMinutes = ((top + height) / hourHeight) * 60; +// // // +// // // startMinutes = Math.round(startMinutes / 15) * 15; +// // // endMinutes = Math.round(endMinutes / 15) * 15; +// // // +// // // let startHour = Math.floor(startMinutes / 60); +// // // let startMin = startMinutes % 60; +// // // +// // // let endHour = Math.floor(endMinutes / 60); +// // // let endMin = endMinutes % 60; +// // // +// // // let startTime = `${String(startHour).padStart(2,'0')}:${String(startMin).padStart(2,'0')}`; +// // // let endTime = `${String(endHour).padStart(2,'0')}:${String(endMin).padStart(2,'0')}`; +// // // +// // // let component = target.closest('[wire\\:id]'); +// // // +// // // if (component) { +// // // Livewire.find(component.getAttribute('wire:id')) +// // // .call('updateEventTimeAndDate', target.dataset.id, column.dataset.date, startTime, endTime); +// // // } +// // // +// // // target.style.transform = ''; +// // // target.dataset.y = 0; +// // // } +// // // } +// // // }); +// // // } +// // // +// // // document.addEventListener('DOMContentLoaded', () => { +// // // initCalendar(); +// // // }); diff --git a/src/resources/js/calendar-week-canvas.js b/src/resources/js/calendar-week-canvas.js new file mode 100644 index 0000000..5c26750 --- /dev/null +++ b/src/resources/js/calendar-week-canvas.js @@ -0,0 +1,385 @@ +/** + * Week-Canvas – Alpine.data-Komponente + * Drag & Drop + Resize fΓΌr Week / Day / Month-Ansicht + * 15-Minuten-Snapping in der Zeitachse + */ + +const CELL_PX = 48; // px pro Stunde (muss mit $cellPx im Blade ΓΌbereinstimmen) +const SLOT_PX = CELL_PX / 4; // 12 px = 15 Minuten + +document.addEventListener('alpine:init', () => { + Alpine.data('weekCanvas', () => ({ + + // ── Focus (Einzelklick β†’ Vordergrund) ──────────────────────── + focusedEventId: null, + + // ── Timed-Event DnD (Week / Day) ───────────────────────────── + draggingId: null, + dragOffsetY: 0, + dragGrabDate: null, // Datum der Kopie, die der User gegriffen hat + + // ── All-Day-Event DnD (Week) ────────────────────────────────── + draggingAllDayId: null, + draggingAllDayStartCol: 0, + draggingAllDayStartDate: null, + draggingAllDayGrabDate: null, // konkretes Grab-Datum aus data-date + allDayGrabColOffset: 0, + allDayDropCol: null, + + // ── Month-View DnD ──────────────────────────────────────────── + monthDraggingId: null, + monthDraggingIsAllDay: false, + monthDraggingHour: 0, + monthDraggingMinute: 0, + monthDraggingStartDate: null, + + // ── Drag-Zeitvorschau (intern) ──────────────────────────────── + hourStart: 0, + _dragTimeEl: null, + _dragOrigTime: '', + _dragDurSlots: 0, + + // ── Init ────────────────────────────────────────────────────── + + init() { + this.hourStart = parseInt(this.$el.dataset.hourStart) || 0; + this.$nextTick(() => { + const g = this.$refs.scrollarea; + if (g) g.scrollTop = 8 * CELL_PX; // zu 08:00 scrollen + }); + }, + + _timeStr(h, m) { + return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0'); + }, + + // ═══════════════════════════════════════════════════════════════ + // CREATE ON DOUBLE-CLICK (Week + Day timeline) + // ═══════════════════════════════════════════════════════════════ + + onCreateAt(e, date) { + const rect = e.currentTarget.getBoundingClientRect(); + const relY = Math.max(0, e.clientY - rect.top); + const slot = Math.floor(relY / SLOT_PX); + const hour = Math.min(Math.floor(slot / 4), 23); + const minute = (slot % 4) * 15; + this.$wire.createEventAt(date, hour, minute); + }, + + // ═══════════════════════════════════════════════════════════════ + // TIMED-EVENT Drag & Drop (Week + Day) + // ═══════════════════════════════════════════════════════════════ + + onDragStart(e, id) { + this.draggingId = id; + this.focusedEventId = null; + const rect = e.currentTarget.getBoundingClientRect(); + this.dragOffsetY = e.clientY - rect.top; + // Datum der gegriffenen Kopie (data-grab-date gesetzt im Blade) + this.dragGrabDate = e.currentTarget.dataset.grabDate || null; + e.dataTransfer.effectAllowed = 'move'; + + // Klon erstellen der der Maus folgt (statt statischem Browser-Ghost) + const clone = e.currentTarget.cloneNode(true); + // Alpine-Attribute entfernen (Klon lebt außerhalb des x-data Scopes) + clone.querySelectorAll('[x-bind\\:class],[x-bind\\:style],[x-on\\:click],[x-on\\:dblclick],[x-on\\:dragstart],[x-on\\:dragend],[x-on\\:mousedown]').forEach(el => { + [...el.attributes].forEach(a => { if (a.name.startsWith('x-') || a.name.startsWith(':') || a.name.startsWith('@')) el.removeAttribute(a.name); }); + }); + [...clone.attributes].forEach(a => { if (a.name.startsWith('x-') || a.name.startsWith(':') || a.name.startsWith('@')) clone.removeAttribute(a.name); }); + clone.removeAttribute('wire:key'); + clone.removeAttribute('draggable'); + clone.style.position = 'fixed'; + clone.style.width = rect.width + 'px'; + clone.style.height = rect.height + 'px'; + clone.style.left = rect.left + 'px'; + clone.style.top = rect.top + 'px'; + clone.style.zIndex = '9999'; + clone.style.pointerEvents = 'none'; + clone.style.opacity = '0.92'; + clone.style.boxShadow = '0 4px 16px rgba(0,0,0,0.18)'; + clone.style.transition = 'none'; + clone.id = 'drag-clone'; + document.body.appendChild(clone); + + this._dragClone = clone; + this._dragCloneTimeEl = clone.querySelector('[data-time-display]'); + this._dragDurSlots = Math.round(e.currentTarget.offsetHeight / SLOT_PX); + this._dragOffsetX = e.clientX - rect.left; + + // Nativen Ghost verstecken + const ghost = document.createElement('div'); + ghost.style.cssText = 'position:fixed;top:-999px;left:-999px;width:1px;height:1px;opacity:0;'; + document.body.appendChild(ghost); + e.dataTransfer.setDragImage(ghost, 0, 0); + requestAnimationFrame(() => document.body.removeChild(ghost)); + + setTimeout(() => e.target.style.opacity = '0.15', 0); + }, + + onDragEnd(e) { + e.target.style.opacity = '1'; + if (this._dragClone) { this._dragClone.remove(); this._dragClone = null; } + this._dragCloneTimeEl = null; + this.draggingId = null; + this.dragOffsetY = 0; + this.dragGrabDate = null; + this._dragDurSlots = 0; + }, + + onDragOver(e) { + e.preventDefault(); + e.currentTarget.style.background = 'rgba(99,102,241,0.07)'; + + // Klon der Maus folgen lassen + Zeit live aktualisieren + if (this._dragClone && e.clientY > 0) { + this._dragClone.style.left = (e.clientX - this._dragOffsetX) + 'px'; + this._dragClone.style.top = (e.clientY - this.dragOffsetY) + 'px'; + + const rect = e.currentTarget.getBoundingClientRect(); + const relY = Math.max(0, e.clientY - rect.top - this.dragOffsetY); + const slot = Math.round(relY / SLOT_PX); + const hour = Math.min(Math.floor(slot / 4) + this.hourStart, 23); + const minute = (slot % 4) * 15; + const endSlot = slot + this._dragDurSlots; + const endH = Math.min(Math.floor(endSlot / 4) + this.hourStart, 24); + const endM = (endSlot % 4) * 15; + + if (this._dragCloneTimeEl) { + this._dragCloneTimeEl.textContent = this._timeStr(hour, minute) + ' – ' + this._timeStr(endH, endM); + } + } + }, + + onDragLeave(e) { + if (e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) return; + e.currentTarget.style.background = ''; + }, + + onDrop(e, date) { + e.preventDefault(); + e.currentTarget.style.background = ''; + if (!this.draggingId) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const relY = Math.max(0, e.clientY - rect.top - this.dragOffsetY); + const slot = Math.round(relY / SLOT_PX); + const hour = Math.min(Math.floor(slot / 4), 23); + const minute = (slot % 4) * 15; + + // dragGrabDate: Datum der gegriffenen Kopie β†’ PHP berechnet dayDelta relativ + this.$wire.moveEvent(this.draggingId, date, hour, minute, this.dragGrabDate); + this.draggingId = null; + this.dragOffsetY = 0; + this.dragGrabDate = null; + }, + + // ═══════════════════════════════════════════════════════════════ + // RESIZE (Week + Day) + // ═══════════════════════════════════════════════════════════════ + + onResizeStart(e, id) { + e.preventDefault(); + + const wire = this.$wire; + const startY = e.clientY; + const eventEl = e.currentTarget.parentElement; + const startH = eventEl.offsetHeight; + const topPx = eventEl.offsetTop; + const hourStart = this.hourStart; + const timeEl = eventEl.querySelector('[data-time-display]'); + const origTime = timeEl ? timeEl.textContent : ''; + + const startSlot = Math.round(topPx / SLOT_PX); + const sH = Math.min(Math.floor(startSlot / 4) + hourStart, 23); + const sM = (startSlot % 4) * 15; + + eventEl.style.transition = 'none'; + document.body.style.userSelect = 'none'; + document.body.style.cursor = 'ns-resize'; + + const onMove = (mv) => { + const snapped = Math.round((mv.clientY - startY) / SLOT_PX) * SLOT_PX; + const newHeight = Math.max(SLOT_PX, startH + snapped); + eventEl.style.height = newHeight + 'px'; + + if (timeEl) { + const endSlot = Math.round((topPx + newHeight) / SLOT_PX); + const endH = Math.min(Math.floor(endSlot / 4) + hourStart, 24); + const endM = (endSlot % 4) * 15; + timeEl.textContent = this._timeStr(sH, sM) + ' – ' + this._timeStr(endH, endM); + } + }; + + const onUp = (up) => { + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + eventEl.style.transition = ''; + + const delta = Math.round((up.clientY - startY) / SLOT_PX) * 0.25; + + if (Math.abs(delta) >= 0.25) { + wire.resizeEvent(id, delta); + } else { + eventEl.style.height = startH + 'px'; + if (timeEl) timeEl.textContent = origTime; + } + + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + }; + + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }, + + // ═══════════════════════════════════════════════════════════════ + // ALL-DAY DnD (Week – X-Achse) + // ═══════════════════════════════════════════════════════════════ + + onAllDayDragStart(e, id, startCol, startDate) { + this.draggingAllDayId = id; + this.draggingAllDayStartCol = startCol; + this.draggingAllDayStartDate = startDate; + + const strip = this.$refs.allDayStrip; + if (strip) { + const stripRect = strip.getBoundingClientRect(); + const grabCol = Math.min(6, Math.max(0, Math.floor((e.clientX - stripRect.left) / stripRect.width * 7))); + // Grab-Datum aus data-date des Spalten-Divs lesen (exakte Datumsreferenz) + const colEl = strip.querySelector(`[data-col="${grabCol}"]`); + this.draggingAllDayGrabDate = colEl ? colEl.dataset.date : startDate; + // Offset fΓΌr visuelle Preview (Spalten-basiert) + this.allDayGrabColOffset = Math.max(0, grabCol - startCol); + } else { + this.draggingAllDayGrabDate = startDate; + this.allDayGrabColOffset = 0; + } + + e.dataTransfer.effectAllowed = 'move'; + const ghost = document.createElement('div'); + ghost.style.cssText = 'position:fixed;top:-999px;left:-999px;width:1px;height:1px;opacity:0;'; + document.body.appendChild(ghost); + e.dataTransfer.setDragImage(ghost, 0, 0); + requestAnimationFrame(() => document.body.removeChild(ghost)); + + setTimeout(() => e.target.style.opacity = '0.45', 0); + }, + + onAllDayDragEnd(e) { + e.target.style.opacity = '1'; + this.draggingAllDayId = null; + this.allDayDropCol = null; + this.allDayGrabColOffset = 0; + }, + + onAllDayDragOver(e) { + e.preventDefault(); + if (!this.draggingAllDayId) return; + + const strip = this.$refs.allDayStrip; + if (!strip) return; + + const rect = strip.getBoundingClientRect(); + const rawCol = Math.floor((e.clientX - rect.left) / rect.width * 7); + const targetStart = Math.max(0, Math.min(6, rawCol - this.allDayGrabColOffset)); + this.allDayDropCol = targetStart; + + e.dataTransfer.dropEffect = 'move'; + }, + + onAllDayDragLeave(e) { + if (e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) return; + this.allDayDropCol = null; + }, + + onAllDayDrop(e) { + e.preventDefault(); + if (!this.draggingAllDayId) return; + + const strip = this.$refs.allDayStrip; + if (!strip) return; + + const rect = strip.getBoundingClientRect(); + const rawCol = Math.min(6, Math.max(0, Math.floor((e.clientX - rect.left) / rect.width * 7))); + + // Datum des Ziel-Spalte aus data-date lesen β†’ exakte Berechnung, keine Spalten-Clipping-Fehler + const colEl = strip.querySelector(`[data-col="${rawCol}"]`); + const dropDate = colEl ? colEl.dataset.date : null; + + if (dropDate && this.draggingAllDayGrabDate) { + const dayDelta = Math.round( + (new Date(dropDate + 'T00:00:00') - new Date(this.draggingAllDayGrabDate + 'T00:00:00')) + / 86400000 + ); + if (dayDelta !== 0) { + this.$wire.moveAllDayEvent(this.draggingAllDayId, dayDelta); + } + } + + this.draggingAllDayId = null; + this.draggingAllDayGrabDate = null; + this.allDayDropCol = null; + this.allDayGrabColOffset = 0; + }, + + // ═══════════════════════════════════════════════════════════════ + // MONTH-VIEW DnD (timed + all-day Events) + // ═══════════════════════════════════════════════════════════════ + + onMonthEventDragStart(e, id, hour, minute, isAllDay, startDate) { + this.monthDraggingId = id; + this.monthDraggingIsAllDay = isAllDay; + this.monthDraggingHour = hour; + this.monthDraggingMinute = minute; + this.monthDraggingStartDate = startDate; + this.focusedEventId = null; + e.dataTransfer.effectAllowed = 'move'; + setTimeout(() => e.target.style.opacity = '0.4', 0); + }, + + onMonthEventDragEnd(e) { + e.target.style.opacity = '1'; + this.monthDraggingId = null; + }, + + onMonthDayDragOver(e) { + if (!this.monthDraggingId) return; + e.preventDefault(); + e.currentTarget.style.background = 'rgba(99,102,241,0.07)'; + e.dataTransfer.dropEffect = 'move'; + }, + + onMonthDayDragLeave(e) { + if (e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) return; + e.currentTarget.style.background = ''; + }, + + onMonthDayDrop(e, date) { + e.preventDefault(); + e.currentTarget.style.background = ''; + if (!this.monthDraggingId) return; + + if (this.monthDraggingIsAllDay) { + // Tagesgenaues Verschieben via dayDelta + const start = new Date(this.monthDraggingStartDate + 'T00:00:00'); + const drop = new Date(date + 'T00:00:00'); + const delta = Math.round((drop - start) / 86400000); + if (delta !== 0) { + this.$wire.moveAllDayEvent(this.monthDraggingId, delta); + } + } else { + // grabDate = monthDraggingStartDate ($dayStr der gegriffenen Kopie) + // β†’ PHP berechnet dayDelta relativ und verschiebt Start+End + this.$wire.moveEvent( + this.monthDraggingId, + date, + this.monthDraggingHour, + this.monthDraggingMinute, + this.monthDraggingStartDate + ); + } + + this.monthDraggingId = null; + }, + })); +}); diff --git a/src/resources/js/websocket/connection.js b/src/resources/js/websocket/connection.js new file mode 100644 index 0000000..ebccf64 --- /dev/null +++ b/src/resources/js/websocket/connection.js @@ -0,0 +1,17 @@ +import Echo from 'laravel-echo'; +import Pusher from 'pusher-js'; +window.Pusher = Pusher; + +window.Pusher && (window.Pusher.logToConsole = false); // Debug an + +window.Echo = new Echo({ + broadcaster: 'reverb', + key: import.meta.env.VITE_REVERB_APP_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST, + wsPort: Number(import.meta.env.VITE_REVERB_PORT), + wssPort: Number(import.meta.env.VITE_REVERB_PORT), + forceTLS: import.meta.env.VITE_REVERB_SCHEME === 'https', + disableStats: true, + enabledTransports: import.meta.env.VITE_REVERB_SCHEME === 'https' + ? ['wss'] : ['ws'], +}); diff --git a/src/resources/plugins/Notification/index.js b/src/resources/plugins/Notification/index.js new file mode 100644 index 0000000..2033437 --- /dev/null +++ b/src/resources/plugins/Notification/index.js @@ -0,0 +1,2 @@ +import './src/config.js'; +import './src/message.js'; diff --git a/src/resources/plugins/Notification/src/config.js b/src/resources/plugins/Notification/src/config.js new file mode 100644 index 0000000..a6c27e2 --- /dev/null +++ b/src/resources/plugins/Notification/src/config.js @@ -0,0 +1,86 @@ +/* +|-------------------------------------------------------------------------- +| Notification Toast link +|-------------------------------------------------------------------------- +| +| Import Notification Function. +| +*/ + +import "./message.js"; + +/* +|-------------------------------------------------------------------------- +| Notification Toast config +|-------------------------------------------------------------------------- +| +| Set your configuration for the Notification Toasts.. +| +| type: 'default', // default, success, info, warning, error, update +| mode: 'default-mode', // default-mode, light-mode, dark-mode +| position: 'top-right', // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right +| duration: 3000, // duration in milliseconds 3000 = 3s +| selector: 'body', // Select Tag, Class or id to display the Notification div +| icon: null, // Set true for default-icons or insert an SVG or image with +| progressbar: false, // display Close Progressbar = true / false +| close: false, // display Close button = true / false +| title: 'Hi Guys', // set custom displayed Text +| text: 'Hi Guys', // set custom displayed Text +| onComplete: null, // Callback funktion +| onCompleteParams: {}, // Callback function Parameter +| offset: { // Set your own offset for the Notification div. +| top: '', +| left: '', +| right: '', +| bottom: '', +| }, +| style: { // Set your own style for the Notification div. +| color: '', +| background: '', +| }, +| animation: 'slide-right', // slide-right, slide-left, slide-up, slide-down +| classname: '' // Set custom classes +| +| +*/ + +message.config({ + mode: "light-mode", + icon: true, + position: "bottom-center", + close: true, + animation: "slide-up", + duration: 5000, +}); + +// message.toast({ +// type: "info", +// title: "Hi Guys", +// text: "Hi Guys", +// }) +// +// message.toast({ +// type: 'success', +// title: "Hi Guys", +// text: 'Das ist noch ein Test', +// duration: 5000, +// }) +// +// message.toast({ +// type: 'info', +// title: "Hi Guys", +// text: 'Das ist noch ein Test' +// +// }) +// +// message.toast({ +// type: 'warning', +// title: "Hi Guys", +// text: 'Das ist noch ein Test', +// }) +// +// message.toast({ +// type: 'error', +// title: "Hi Guys", +// text: 'Das ist noch ein Test', +// }) diff --git a/src/resources/plugins/Notification/src/message.css b/src/resources/plugins/Notification/src/message.css new file mode 100644 index 0000000..5551a3d --- /dev/null +++ b/src/resources/plugins/Notification/src/message.css @@ -0,0 +1,380 @@ +* { + --toast-white: 255, 255, 255; + --toast-black: 28, 28, 30; + + --toast-green: 102, 187, 106; + --toast-red: 239, 83, 80; + --toast-orange: 255, 167, 38; + --toast-yellow: 212, 194, 129; + + --toast-nutral: 220, 220, 220; + --toast-gray: 245, 245, 245; + --toast-dark-gray: 120, 120, 120; + + --toast-lightmode: #ffffff; + --toast-darkmode: #1b1b1e; + + --toast-progressbar: #c7c7c7; + + --box-shaddow-inset: none; + --box-shaddow-outset: 0 4px 24px rgba(0, 0, 0, 0.08); +} + +/* ============================= */ +/* POSITION */ +/* ============================= */ + +.notification { + position: fixed; + z-index: 999999; + min-width: 320px; + max-width: 380px; + width: fit-content; +} + +.top-right { top: 24px; right: 24px; } +.top-center { top: 24px; left: 50%; transform: translateX(-50%); } +.bottom-left { bottom: 24px; left: 24px; } +.bottom-right { bottom: 24px; right: 24px; } +.bottom-center { bottom: 24px; left: 50%; transform: translateX(-50%); } +.top-left { top: 24px; left: 24px; } + +/* ============================= */ +/* CONTAINER */ +/* ============================= */ + +.notification-container { + background: rgb(var(--surface, 255 255 255)); + border: 1px solid rgb(var(--border, 230 230 230)); + border-radius: 16px; + width: fit-content; + margin-top: 8px; + overflow: hidden; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); + + transition: opacity 300ms ease-in-out, transform 300ms; +} + +.notification-container:before { + content: ""; + position: absolute; + width: 30px; + height: 30px; + filter: blur(20px); + top: 50%; + transform: translateY(-50%); + left: -20px; + border-radius: 100%; +} + +/* ============================= */ +/* CONTENT */ +/* ============================= */ + +.notification-block { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + padding-right: 44px; +} + +.notification-title { + font-size: 14px; + font-weight: 600; + letter-spacing: -0.01em; +} + +.notification-message { + font-size: 13px; + color: inherit; +} + +/* ============================= */ +/* ICON */ +/* ============================= */ + +.notification-icon { + min-width: 1.7rem; + border-radius: 10px; + padding: 6px; +} + +/* ============================= */ +/* DISMISS */ +/* ============================= */ + +.notification-dismiss-btn { + position: absolute; + top: 10px; + right: 10px; + border-radius: 6px; + transition: background 200ms; +} + +.notification-dismiss-btn:hover { + background: rgba(0,0,0,0.05); +} + +.notification-dismiss-btn svg { + width: 1.2rem; +} + +/* ============================= */ +/* TOAST TYPES */ +/* ============================= */ + +.notification-toast-default { + background: #fff; + color: rgba(var(--toast-black)); +} + +/* β€” SUCCESS β€” */ +.notification-toast-success { + background: #F0FDF4; + border: 1px solid #BBF7D0; + color: #166534; +} + +.notification-toast-success svg path { + stroke: #16A34A; +} + +.notification-toast-success .notification-icon { + background: #DCFCE7; +} + +.notification-toast-success .notification-title { + color: #15803D; +} + +.notification-toast-success:before { + background: #DCFCE7; +} + +/* β€” WARNING β€” */ +.notification-toast-warning { + background: #FFFBEB; + border: 1px solid #FDE68A; + color: #92400E; +} + +.notification-toast-warning svg path { + stroke: #D97706; +} + +.notification-toast-warning .notification-icon { + background: #FEF3C7; +} + +.notification-toast-warning .notification-title { + color: #B45309; +} + +.notification-toast-warning:before { + background: #FEF3C7; +} + +/* β€” INFO β€” */ +.notification-toast-info { + background: #EFF6FF; + border: 1px solid #BFDBFE; + color: #1E40AF; +} + +.notification-toast-info svg path { + stroke: #2563EB; +} + +.notification-toast-info .notification-icon { + background: #DBEAFE; +} + +.notification-toast-info .notification-title { + color: #1D4ED8; +} + +.notification-toast-info:before { + background: #DBEAFE; +} + +/* β€” ERROR β€” */ +.notification-toast-error { + background: #FEF2F2; + border: 1px solid #FECACA; + color: #991B1B; +} + +.notification-toast-error svg path { + stroke: #DC2626; +} + +.notification-toast-error .notification-icon { + background: #FEE2E2; +} + +.notification-toast-error .notification-title { + color: #B91C1C; +} + +.notification-toast-error:before { + background: #FEE2E2; +} + +/* ============================= */ +/* LIGHT / DARK */ +/* ============================= */ + +.light-mode .notification-toast-default { + background: #fff; + color: rgba(var(--toast-black)); +} + +.light-mode .notification-toast-success { + background: #F0FDF4; + color: #166534; +} + +.light-mode .notification-toast-warning { + background: #FFFBEB; + color: #92400E; +} + +.light-mode .notification-toast-info { + background: #EFF6FF; + color: #1E40AF; +} + +.light-mode .notification-toast-error { + background: #FEF2F2; + color: #991B1B; +} + +.dark-mode .notification-toast-default, +.dark-mode .notification-toast-success, +.dark-mode .notification-toast-warning, +.dark-mode .notification-toast-info, +.dark-mode .notification-toast-error { + background: var(--toast-darkmode); + color: #fff; +} + +/* ============================= */ +/* PROGRESSBAR */ +/* ============================= */ + +.notification-progressbar { + position: absolute; + bottom: 0; + right: 0; + height: 3px; + width: 0; + opacity: 0.7; + border-radius: 2px; +} + +.notification-toast-default .notification-progressbar { + background: var(--toast-progressbar); +} + +.notification-toast-success .notification-progressbar { + background: #16A34A; +} + +.notification-toast-warning .notification-progressbar { + background: #D97706; +} + +.notification-toast-error .notification-progressbar { + background: #DC2626; +} + +.notification-toast-info .notification-progressbar { + background: #2563EB; +} + +/* ============================= */ +/* ALIGN */ +/* ============================= */ + +.top-right .notification-container, +.bottom-right .notification-container { + margin-left: auto; +} + +.top-left .notification-container, +.bottom-left .notification-container { + margin-right: auto; +} + +.top-center .notification-container, +.bottom-center .notification-container { + margin-left: auto; + margin-right: auto; +} + +/* ============================= */ +/* ANIMATION */ +/* ============================= */ + +.notification-animation { + padding: 0.125rem; + animation: update 1s linear infinite; +} + +@keyframes update { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.slide-in.slide-down { animation: slideInDown 250ms ease; } +.slide-out.slide-down { animation: slideOutDown 250ms ease; } + +@keyframes slideInDown { + from { transform: translateY(-16px); opacity: 0; } + to { transform: unset; opacity: 1; } +} + +@keyframes slideOutDown { + from { transform: unset; opacity: 1; } + to { transform: translateY(-16px); opacity: 0; } +} + +.slide-in.slide-up { animation: slideInUp 250ms ease; } +.slide-out.slide-up { animation: slideOutUp 250ms ease; } + +@keyframes slideInUp { + from { transform: translateY(16px); opacity: 0; } + to { transform: unset; opacity: 1; } +} + +@keyframes slideOutUp { + from { transform: unset; opacity: 1; } + to { transform: translateY(16px); opacity: 0; } +} + +.slide-in.slide-right { animation: slideInRight 250ms ease; } +.slide-out.slide-right { animation: slideOutRight 250ms ease; } + +@keyframes slideInRight { + from { transform: translateX(100vh); opacity: 0; } + to { transform: unset; opacity: 1; } +} + +@keyframes slideOutRight { + from { transform: unset; opacity: 1; } + to { transform: translateX(100vh); opacity: 0; } +} + +.slide-in.slide-left { animation: slideInLeft 250ms ease; } +.slide-out.slide-left { animation: slideOutLeft 250ms ease; } + +@keyframes slideInLeft { + from { transform: translateX(-100vh); opacity: 0; } + to { transform: unset; opacity: 1; } +} + +@keyframes slideOutLeft { + from { transform: unset; opacity: 1; } + to { transform: translateX(-100vh); opacity: 0; } +} diff --git a/src/resources/plugins/Notification/src/message.js b/src/resources/plugins/Notification/src/message.js new file mode 100644 index 0000000..213bd50 --- /dev/null +++ b/src/resources/plugins/Notification/src/message.js @@ -0,0 +1,251 @@ +function Messages() { + let options = {}; + let globals = { + type: "default", + mode: "default-mode", + position: "top-right", + duration: 3000, + selector: "body", + icon: null, + progressbar: false, + close: false, + title: "", + text: "Hi Guys, how are you?", + onComplete: null, + onCompleteParams: {}, + offset: { + top: "", + left: "", + right: "", + bottom: "", + }, + style: { + color: "", + background: "", + }, + animation: "slide-right", + classname: "", + }; + + const icons = { + default: + '', + success: + '', + info: '', + warning: + '', + error: '', + update: '', + }; + + function config(default_option = {}) { + Object.assign(globals, default_option); + } + + function toast(option) { + options = { ...globals, ...option }; + + let animationTime = 500; + + let notification = document.createElement("div"); + notification.className = `notification ${options.position} ${options.mode} ${options.classname}`; + notification.id = "notification"; + + Object.keys(options.offset).forEach((key) => { + notification.style[key] = options.offset[key]; + }); + + let notificationContainer = document.createElement("div"); + notificationContainer.className = `notification-container notification-toast-${options.type} ${options.animation} ${options.classname}`; + + Object.keys(options.style).forEach((key) => { + notificationContainer.style[key] = options.style[key]; + }); + + // setTimeout(() => { + // notificationContainer.classList.remove(options.animation); + // notificationContainer.style.opacity = '0.95'; + // }, 100); + + // setTimeout(() => { + // }, 100); + + notificationContainer.classList.add("slide-in"); + + // notificationContainer.addEventListener('animationend', () => { + // notificationContainer.classList.remove(options.animation); + // }); + + let notificationBlock = document.createElement("div"); + notificationBlock.className = "notification-block"; + notificationContainer.appendChild(notificationBlock); + + if (options.icon !== null) { + let notificationIcon = document.createElement("div"); + notificationIcon.className = "notification-icon"; + notificationIcon.innerHTML = icons[options.type] ?? icons.default; + notificationBlock.appendChild(notificationIcon); + } + + let notificationMessage = document.createElement("div"); + notificationMessage.className = "notification-message"; + notificationBlock.appendChild(notificationMessage); + + if (options.title.length !== 0) { + let notificationTitle = document.createElement("div"); + notificationTitle.className = "notification-title"; + notificationTitle.innerHTML = "" + options.title + ""; + notificationMessage.appendChild(notificationTitle); + } + let notificationText = document.createElement("div"); + notificationText.className = "notification-text"; + notificationText.innerHTML = "" + options.text + ""; + notificationMessage.appendChild(notificationText); + + if (options.close) { + let notificationDismissButton = document.createElement("button"); + notificationDismissButton.className = "notification-dismiss-btn"; + notificationDismissButton.innerHTML = + ''; + notificationBlock.appendChild(notificationDismissButton); + + notificationDismissButton.addEventListener("click", () => { + // notificationContainer.classList.add(options.animation); + + notificationContainer.classList.remove("slide-in"); + notificationContainer.classList.add("slide-out"); + + notificationContainer.addEventListener("animationend", () => { + notificationContainer.remove(); + }); + }); + } + + if (options.progressbar && options.duration !== -1) { + notificationContainer.addEventListener("animationend", () => { + let currentProgress = 0; + let notificationProgressbar = document.createElement("div"); + notificationProgressbar.className = "notification-progressbar"; + notificationBlock.appendChild(notificationProgressbar); + + let count = setInterval(() => { + if (currentProgress < 100) { + currentProgress += 1; + notificationProgressbar.dataset.duration = (options.duration ?? globals.duration); + notificationProgressbar.style.width = "100%"; + // currentProgress + "%"; + } else { + clearInterval(count); + } + }, (options.duration ?? globals.duration) / 100); + }); + } + + if (document.getElementById(notification.id) === null) { + document.querySelector(options.selector).appendChild(notification); + } + + document + .getElementById(notification.id) + .insertBefore( + notificationContainer, + document.getElementById(notification.id).children[0], + ); + + function removeToast() { + notificationContainer.classList.remove("slide-in"); + notificationContainer.classList.add("slide-out"); + notificationContainer.addEventListener("animationend", () => { + notificationContainer.remove(); + }); + } + + if ( + typeof options.onComplete === "string" && + typeof window.Callbacks[options.onComplete] === "function" + ) { + window.Callbacks[options.onComplete](options.onCompleteParams); + } else { + for (const [key, value] of Object.entries(options)) { + setTimeout(() => { + if (key === "duration" && value !== -1) + { + removeToast(); + } + }, (options.duration ?? globals.duration) + animationTime); + } + } + } + + return { + toast, + config, + }; +} + +window.message = Messages(); + +// // message.config() +// message.config({ +// // 'type': 'info', +// // 'icon': '123123', +// 'icon': true, +// 'position': 'bottom-center', +// 'progressbar': true, +// 'animation': 'slide-up', +// // 'mode': 'light-mode', +// // 'duration': -1, +// // 'offset': { +// // 'top': '100px', +// // 'right': '80px', +// // } +// }) + +// message.toast({ +// 'type': 'update', +// // 'icon': '123123', +// // 'position': 'top-center', +// 'text': 'Das ist ein Test', +// // 'duration': -1, +// 'close': true, +// // 'onComplete': 'test', +// // 'onCompleteParams': { +// // params: 'test', +// // }, +// +// +// // 'style': { +// // background:'blue', +// // color: 'red' +// // } +// }) +// +// message.toast({ +// 'type': 'success', +// 'text': 'Das ist noch ein Test', +// // 'onComplete': 'test1', +// +// +// }) +// +// message.toast({ +// 'type': 'info', +// 'text': 'Das ist noch ein Test' +// +// }) +// +// message.toast({ +// 'type': 'warning', +// 'text': 'Das ist noch ein Test', +// // 'duration': -1 +// // 'onComplete': 'test2', +// +// +// }) +// +// message.toast({ +// 'type': 'error', +// 'text': 'Anderes', +// // 'duration': -1 +// }) diff --git a/src/resources/views/components/app-header.blade.php b/src/resources/views/components/app-header.blade.php new file mode 100644 index 0000000..73387b7 --- /dev/null +++ b/src/resources/views/components/app-header.blade.php @@ -0,0 +1,36 @@ +@props([ + 'icon' => null, + 'title' => null, + 'subtitle' => null, + 'actions' => null, +]) + +