Release v1.0.5: Bugfix-Welle (Manual-Job-Form, :app_name, Logging, DSGVO-EN, Icons)
Sammelrelease nach Feedback aus der Test-Installation. Sieben unabhängige
Probleme — vom Form-Breaker über Übersetzungs-Lücken bis Log-Hygiene.
- fix(jobs): Einsatz manuell erfassen brach das HTML-Attribut, sobald
Customer-Namen ein " enthielten (@json in x-data=""). Auf json_encode
via {{ }} umgestellt (HTML-escaped). Alpine initialisiert wieder,
Anlegen-Button funktioniert.
- feat(i18n): BrandedTranslator injiziert :app_name automatisch in alle
__()-Aufrufe. Hilfetexte, Mails, Update-Strings rendern jetzt überall
korrekt 'Schneespur' bzw. 'Wintertrace' statt rohes :app_name.
- feat(dsgvo): EN-Default-Vorlage default-template-en.blade.php mit
UK/EU GDPR-Wording (Art. 6(1)(f), ICO-Hinweis, Subject Rights).
Controller laden locale-aware mit Fallback auf DE. Placeholder-Helper
kennt DE + EN Tokens.
- ui(settings): Alle 8 Settings-Karten haben jetzt Icons, nicht nur
Module. Markup auf array-driven Loop entrümpelt.
- chore(modules): Example-Modul boot()-gated via EXAMPLE_MODULE_ENABLED
env-Var (default false). Aus Release-ZIP komplett entfernt. Bestehende
Installs mit altem example/-Ordner laden es nicht mehr automatisch.
- chore(logging): ModuleManager Discovery/Boot-Logs auf debug
runtergesetzt (waren info → fluteten laravel.log bei jedem Request).
Defaults auf daily-Rotation mit 14d Retention + LOG_LEVEL=warning für
Production.
- fix(install): Hardcoded deutsche Fehlermeldung im InstallerController
durch __('install.preflight_has_failures') ersetzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ed375efb22
commit
182f1b4c98
17 changed files with 254 additions and 99 deletions
3
build.sh
3
build.sh
|
|
@ -54,9 +54,10 @@ cp -r "$SOURCE_DIR/resources" "$BUILD_DIR/"
|
|||
cp -r "$SOURCE_DIR/routes" "$BUILD_DIR/"
|
||||
cp -r "$SOURCE_DIR/vendor" "$BUILD_DIR/"
|
||||
|
||||
# Modules directory (example module for reference)
|
||||
# Modules directory — example module is dev-only and excluded from releases
|
||||
if [ -d "$SOURCE_DIR/modules" ]; then
|
||||
cp -r "$SOURCE_DIR/modules" "$BUILD_DIR/"
|
||||
rm -rf "$BUILD_DIR/modules/example"
|
||||
fi
|
||||
|
||||
# Documentation and legal → build root (flat, alongside code)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@ APP_MAINTENANCE_DRIVER=file
|
|||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_STACK=daily
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
LOG_LEVEL=warning
|
||||
LOG_DAILY_DAYS=14
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.0.4
|
||||
1.0.5
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\DsgvoOnboardingController;
|
||||
use App\Models\DsgvoConfirmation;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -18,7 +19,7 @@ class DsgvoAdminController extends Controller
|
|||
$version = (int) Setting::get('dsgvo_template_version', 1);
|
||||
|
||||
if ($markdown === null) {
|
||||
$markdown = view('dsgvo.default-template')->render();
|
||||
$markdown = view(DsgvoOnboardingController::resolveDefaultTemplateView())->render();
|
||||
}
|
||||
|
||||
$previewHtml = Str::markdown($this->replacePlaceholders($markdown), ['html_input' => 'strip']);
|
||||
|
|
@ -82,25 +83,7 @@ class DsgvoAdminController extends Controller
|
|||
|
||||
private function replacePlaceholders(string $text): string
|
||||
{
|
||||
$companyName = Setting::get('company_name', '');
|
||||
$street = Setting::get('company_street', '');
|
||||
$zip = Setting::get('company_zip', '');
|
||||
$city = Setting::get('company_city', '');
|
||||
$email = Setting::get('company_email', '');
|
||||
$dpo = Setting::get('dpo_contact', '');
|
||||
$dpoEmail = Setting::get('dpo_email', '');
|
||||
|
||||
$address = trim("$street, $zip $city", ', ');
|
||||
|
||||
$replacements = [
|
||||
'[Firmenname eintragen]' => $companyName ?: '[Firmenname eintragen]',
|
||||
'[Adresse eintragen]' => $address ?: '[Adresse eintragen]',
|
||||
'[E-Mail-Adresse eintragen]' => $email ?: '[E-Mail-Adresse eintragen]',
|
||||
'[DPO-E-Mail-Adresse eintragen]' => $dpoEmail ?: '[DPO-E-Mail-Adresse eintragen]',
|
||||
'[Datenschutzbeauftragter / Ansprechpartner eintragen]' => $dpo ?: '[Datenschutzbeauftragter / Ansprechpartner eintragen]',
|
||||
];
|
||||
|
||||
return str_replace(array_keys($replacements), array_values($replacements), $text);
|
||||
return dsgvo_apply_company_placeholders($text);
|
||||
}
|
||||
|
||||
public function showConfirmation(int $id): View
|
||||
|
|
|
|||
|
|
@ -66,32 +66,22 @@ class DsgvoOnboardingController extends Controller
|
|||
$version = (int) Setting::get('dsgvo_template_version', 1);
|
||||
|
||||
if ($text === null) {
|
||||
$text = view('dsgvo.default-template')->render();
|
||||
$text = view(self::resolveDefaultTemplateView())->render();
|
||||
}
|
||||
|
||||
return [$text, $version];
|
||||
}
|
||||
|
||||
public static function resolveDefaultTemplateView(): string
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
$localized = "dsgvo.default-template-{$locale}";
|
||||
|
||||
return view()->exists($localized) ? $localized : 'dsgvo.default-template';
|
||||
}
|
||||
|
||||
private function replacePlaceholders(string $text): string
|
||||
{
|
||||
$companyName = Setting::get('company_name', '');
|
||||
$street = Setting::get('company_street', '');
|
||||
$zip = Setting::get('company_zip', '');
|
||||
$city = Setting::get('company_city', '');
|
||||
$email = Setting::get('company_email', '');
|
||||
$dpo = Setting::get('dpo_contact', '');
|
||||
$dpoEmail = Setting::get('dpo_email', '');
|
||||
|
||||
$address = trim("$street, $zip $city", ', ');
|
||||
|
||||
$replacements = [
|
||||
'[Firmenname eintragen]' => $companyName ?: '[Firmenname eintragen]',
|
||||
'[Adresse eintragen]' => $address ?: '[Adresse eintragen]',
|
||||
'[E-Mail-Adresse eintragen]' => $email ?: '[E-Mail-Adresse eintragen]',
|
||||
'[DPO-E-Mail-Adresse eintragen]' => $dpoEmail ?: '[DPO-E-Mail-Adresse eintragen]',
|
||||
'[Datenschutzbeauftragter / Ansprechpartner eintragen]' => $dpo ?: '[Datenschutzbeauftragter / Ansprechpartner eintragen]',
|
||||
];
|
||||
|
||||
return str_replace(array_keys($replacements), array_values($replacements), $text);
|
||||
return dsgvo_apply_company_placeholders($text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class InstallerController extends Controller
|
|||
{
|
||||
if ($this->preflightChecker->hasCriticalFailures()) {
|
||||
return redirect()->route('install.preflight')
|
||||
->withErrors(['preflight' => 'Kritische Voraussetzungen nicht erfüllt.']);
|
||||
->withErrors(['preflight' => __('install.preflight_has_failures')]);
|
||||
}
|
||||
|
||||
return redirect()->route('install.database');
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use App\Services\ModuleManager;
|
|||
use App\Services\RetentionService;
|
||||
use App\Services\SchneespurUpdater;
|
||||
use App\Services\SeasonService;
|
||||
use App\Services\Translation\BrandedTranslator;
|
||||
use App\Services\Weather\BrightSkyProvider;
|
||||
use App\Services\Weather\MetNorwayProvider;
|
||||
use App\Services\Weather\OpenMeteoApiProvider;
|
||||
|
|
@ -66,6 +67,17 @@ class AppServiceProvider extends ServiceProvider
|
|||
if (empty(config('app.key'))) {
|
||||
config(['app.key' => 'base64:'.base64_encode(random_bytes(32))]);
|
||||
}
|
||||
|
||||
$this->app->extend('translator', function ($translator, $app) {
|
||||
if ($translator instanceof BrandedTranslator) {
|
||||
return $translator;
|
||||
}
|
||||
$branded = new BrandedTranslator($translator->getLoader(), $translator->getLocale());
|
||||
if ($fallback = $translator->getFallback()) {
|
||||
$branded->setFallback($fallback);
|
||||
}
|
||||
return $branded;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class ModuleManager
|
|||
|
||||
$this->modules[$slug] = $manifest;
|
||||
|
||||
Log::info('ModuleManager: module discovered', [
|
||||
Log::debug('ModuleManager: module discovered', [
|
||||
'slug' => $slug,
|
||||
'version' => $manifest['version'] ?? 'unknown',
|
||||
]);
|
||||
|
|
@ -93,6 +93,13 @@ class ModuleManager
|
|||
continue;
|
||||
}
|
||||
|
||||
// Reference example module — opt-in only via env var. Ensures the
|
||||
// bundled dev demo never auto-loads on customer installs even if the
|
||||
// old folder is still present after upgrading from older releases.
|
||||
if ($slug === 'example' && ! env('EXAMPLE_MODULE_ENABLED', false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$namespace = $manifest['namespace'] ?? null;
|
||||
$srcPath = ($manifest['path'] ?? '') . '/src';
|
||||
|
||||
|
|
@ -128,7 +135,7 @@ class ModuleManager
|
|||
$provider->register();
|
||||
$provider->boot();
|
||||
|
||||
Log::info('ModuleManager: module booted', [
|
||||
Log::debug('ModuleManager: module booted', [
|
||||
'slug' => $slug,
|
||||
'version' => $manifest['version'] ?? 'unknown',
|
||||
]);
|
||||
|
|
@ -165,7 +172,7 @@ class ModuleManager
|
|||
$this->disabledModules[] = $slug;
|
||||
}
|
||||
|
||||
Log::info('ModuleManager: module disabled', ['slug' => $slug]);
|
||||
Log::debug('ModuleManager: module disabled', ['slug' => $slug]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
26
schneespur/app/Services/Translation/BrandedTranslator.php
Normal file
26
schneespur/app/Services/Translation/BrandedTranslator.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Translation;
|
||||
|
||||
use Illuminate\Translation\Translator;
|
||||
|
||||
class BrandedTranslator extends Translator
|
||||
{
|
||||
public function get($key, array $replace = [], $locale = null, $fallback = true)
|
||||
{
|
||||
if (! array_key_exists('app_name', $replace)) {
|
||||
$replace['app_name'] = $this->resolveAppName();
|
||||
}
|
||||
|
||||
return parent::get($key, $replace, $locale, $fallback);
|
||||
}
|
||||
|
||||
private function resolveAppName(): string
|
||||
{
|
||||
try {
|
||||
return brand();
|
||||
} catch (\Throwable) {
|
||||
return (string) config('app.name', 'Schneespur');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,3 +23,46 @@ function brand(): string
|
|||
default => 'Schneespur',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace company/DPO placeholders in DSGVO/GDPR template markdown.
|
||||
*
|
||||
* Knows both German and English placeholder strings so the same Settings
|
||||
* values feed into either template language. Missing settings leave the
|
||||
* placeholder visible so admins notice gaps.
|
||||
*/
|
||||
function dsgvo_apply_company_placeholders(string $text): string
|
||||
{
|
||||
$companyName = Setting::get('company_name', '');
|
||||
$street = Setting::get('company_street', '');
|
||||
$zip = Setting::get('company_zip', '');
|
||||
$city = Setting::get('company_city', '');
|
||||
$email = Setting::get('company_email', '');
|
||||
$dpo = Setting::get('dpo_contact', '');
|
||||
$dpoEmail = Setting::get('dpo_email', '');
|
||||
|
||||
$address = trim("$street, $zip $city", ', ');
|
||||
|
||||
$map = [
|
||||
// German placeholders (default-template.blade.php)
|
||||
'[Firmenname eintragen]' => $companyName,
|
||||
'[Adresse eintragen]' => $address,
|
||||
'[E-Mail-Adresse eintragen]' => $email,
|
||||
'[DPO-E-Mail-Adresse eintragen]' => $dpoEmail,
|
||||
'[Datenschutzbeauftragter / Ansprechpartner eintragen]' => $dpo,
|
||||
// English placeholders (default-template-en.blade.php)
|
||||
'[Company name]' => $companyName,
|
||||
'[Address]' => $address,
|
||||
'[Email]' => $email,
|
||||
'[DPO email]' => $dpoEmail,
|
||||
'[Data Protection Officer / Contact]' => $dpo,
|
||||
];
|
||||
|
||||
foreach ($map as $token => $value) {
|
||||
if ($value !== '') {
|
||||
$text = str_replace($token, $value, $text);
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,21 +54,21 @@ return [
|
|||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'daily')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'level' => env('LOG_LEVEL', 'warning'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'level' => env('LOG_LEVEL', 'warning'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
"version": "1.0.0",
|
||||
"namespace": "Schneespur\\Module\\Example",
|
||||
"service_provider": "Schneespur\\Module\\Example\\ExampleServiceProvider",
|
||||
"description": "Reference module demonstrating all extension points (nav, widget, event, settings, route).",
|
||||
"description": "Reference module demonstrating all extension points (nav, widget, event, settings, route). Dev-only — opt in via EXAMPLE_MODULE_ENABLED=true in .env.",
|
||||
"min_schneespur_version": "1.0.0",
|
||||
"requires_permissions": []
|
||||
"requires_permissions": [],
|
||||
"default_enabled": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ class ExampleServiceProvider extends ServiceProvider
|
|||
|
||||
public function boot(): void
|
||||
{
|
||||
if (! $this->shouldBoot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'example-module');
|
||||
|
||||
$this->registerNavigation();
|
||||
|
|
@ -28,6 +32,15 @@ class ExampleServiceProvider extends ServiceProvider
|
|||
$this->registerRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference module — only loads when explicitly enabled.
|
||||
* Devs can enable for local exploration via .env: EXAMPLE_MODULE_ENABLED=true
|
||||
*/
|
||||
protected function shouldBoot(): bool
|
||||
{
|
||||
return (bool) env('EXAMPLE_MODULE_ENABLED', false);
|
||||
}
|
||||
|
||||
protected function registerNavigation(): void
|
||||
{
|
||||
$nav = $this->app->make(NavigationRegistry::class);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@
|
|||
@endphp
|
||||
<form method="POST" action="{{ route('admin.jobs.manual.store') }}" class="mt-6"
|
||||
x-data="{
|
||||
selectedCustomerId: {{ old('customer_id', 'null') }},
|
||||
selectedObjectId: {{ old('customer_object_id', 'null') }},
|
||||
allObjects: @json($allObjects),
|
||||
selectedCustomerId: {{ old('customer_id') ? (int) old('customer_id') : 'null' }},
|
||||
selectedObjectId: {{ old('customer_object_id') ? (int) old('customer_object_id') : 'null' }},
|
||||
allObjects: {{ json_encode($allObjects) }},
|
||||
get objects() { return this.allObjects.filter(o => o.customer_id === this.selectedCustomerId); },
|
||||
onCustomerChange() {
|
||||
const objs = this.objects;
|
||||
|
|
|
|||
|
|
@ -1,47 +1,74 @@
|
|||
<x-admin-layout>
|
||||
<x-slot name="header">{{ __('admin.page_settings') }} <x-help-icon topic="settings" /></x-slot>
|
||||
|
||||
@php
|
||||
$cards = [
|
||||
[
|
||||
'route' => 'admin.settings.branding',
|
||||
'title' => __('ui.branding_title'),
|
||||
'desc' => __('ui.branding_description'),
|
||||
'icon' => 'M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42',
|
||||
],
|
||||
[
|
||||
'route' => 'admin.settings.email',
|
||||
'title' => __('notification.settings_card_email'),
|
||||
'desc' => __('notification.settings_card_email_desc'),
|
||||
'icon' => 'M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75',
|
||||
],
|
||||
[
|
||||
'route' => 'admin.settings.notification-log',
|
||||
'title' => __('notification.settings_card_log'),
|
||||
'desc' => __('notification.settings_card_log_desc'),
|
||||
'icon' => 'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z',
|
||||
],
|
||||
[
|
||||
'route' => 'admin.settings.company',
|
||||
'title' => __('settings.company_title'),
|
||||
'desc' => __('settings.company_description'),
|
||||
'icon' => 'M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z',
|
||||
],
|
||||
[
|
||||
'route' => 'admin.settings.retention',
|
||||
'title' => __('settings.retention_title'),
|
||||
'desc' => __('settings.retention_description'),
|
||||
'icon' => 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
],
|
||||
[
|
||||
'route' => 'admin.settings.weather',
|
||||
'title' => __('weather.settings_title'),
|
||||
'desc' => __('weather.settings_description'),
|
||||
'icon' => 'M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z',
|
||||
],
|
||||
[
|
||||
'route' => 'admin.settings.update',
|
||||
'title' => __('update.settings_title'),
|
||||
'desc' => __('update.settings_description'),
|
||||
'icon' => 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99',
|
||||
],
|
||||
[
|
||||
'route' => 'admin.settings.modules.index',
|
||||
'title' => __('modules.settings_card_title'),
|
||||
'desc' => __('modules.settings_card_description'),
|
||||
'icon' => 'm21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9',
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="max-w-2xl space-y-4">
|
||||
<a href="{{ route('admin.settings.branding') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ __('ui.branding_title') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ __('ui.branding_description') }}</p>
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.email') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ __('notification.settings_card_email') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ __('notification.settings_card_email_desc') }}</p>
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.notification-log') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ __('notification.settings_card_log') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ __('notification.settings_card_log_desc') }}</p>
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.company') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ __('settings.company_title') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ __('settings.company_description') }}</p>
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.retention') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ __('settings.retention_title') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ __('settings.retention_description') }}</p>
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.weather') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ __('weather.settings_title') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ __('weather.settings_description') }}</p>
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.update') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ __('update.settings_title') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ __('update.settings_description') }}</p>
|
||||
</a>
|
||||
<a href="{{ route('admin.settings.modules.index') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-50 text-indigo-600">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
|
||||
</svg>
|
||||
@foreach ($cards as $card)
|
||||
<a href="{{ route($card['route']) }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-50 text-indigo-600">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="{{ $card['icon'] }}" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ $card['title'] }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ $card['desc'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ __('modules.settings_card_title') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ __('modules.settings_card_description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-admin-layout>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@
|
|||
@endphp
|
||||
<form method="POST" action="{{ route('driver.job.manual.store') }}" class="space-y-5"
|
||||
x-data="{
|
||||
selectedCustomerId: {{ old('customer_id', 'null') }},
|
||||
selectedObjectId: {{ old('customer_object_id', 'null') }},
|
||||
allObjects: @json($allObjects),
|
||||
selectedCustomerId: {{ old('customer_id') ? (int) old('customer_id') : 'null' }},
|
||||
selectedObjectId: {{ old('customer_object_id') ? (int) old('customer_object_id') : 'null' }},
|
||||
allObjects: {{ json_encode($allObjects) }},
|
||||
get objects() { return this.allObjects.filter(o => o.customer_id === this.selectedCustomerId); },
|
||||
onCustomerChange() {
|
||||
const objs = this.objects;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# Data Protection Information for Winter Maintenance Staff
|
||||
|
||||
## 1. Controller
|
||||
|
||||
**[Company name]**
|
||||
[Address]
|
||||
Email: [Email]
|
||||
|
||||
## 2. Purpose of processing
|
||||
|
||||
In the course of your work in winter maintenance, the following personal data is collected and processed:
|
||||
|
||||
- **GPS location data** during operational hours, to document the gritting and snow-clearance services performed
|
||||
- **Operational times** (start and end) for working-time records
|
||||
- **Vehicle assignment** to enable traceability of the vehicles used
|
||||
- **Photographs and notes** as evidence of the work carried out
|
||||
|
||||
## 3. Legal basis
|
||||
|
||||
Processing is carried out on the basis of **Article 6(1)(f) UK GDPR / EU GDPR** (legitimate interests). The legitimate interest of the employer lies in fulfilling the duty to maintain safe traffic conditions and in providing evidence that winter maintenance operations have been carried out properly.
|
||||
|
||||
## 4. Collection of GPS data
|
||||
|
||||
GPS data is collected via the **OwnTracks** app installed on your work device. Location data is recorded only during active operational hours and transmitted to the employer's server. The app does not track you outside of recorded shifts.
|
||||
|
||||
## 5. Retention period
|
||||
|
||||
The data collected is retained for the duration of statutory retention obligations (typically three years, in line with limitation periods for traffic-safety liability claims). After this period the data is deleted or anonymised.
|
||||
|
||||
## 6. Your rights
|
||||
|
||||
Under the UK GDPR / EU GDPR you have the following rights:
|
||||
|
||||
- **Right of access** (Article 15): you may request information about the personal data we hold about you.
|
||||
- **Right to rectification** (Article 16): you may request correction of inaccurate data.
|
||||
- **Right to erasure** (Article 17): you may request deletion of your data, subject to overriding legal retention obligations.
|
||||
- **Right to restriction of processing** (Article 18)
|
||||
- **Right to data portability** (Article 20)
|
||||
- **Right to object** (Article 21)
|
||||
- **Right to lodge a complaint** with the competent supervisory authority (ICO in the UK; the national DPA in other jurisdictions).
|
||||
|
||||
## 7. Contact for data protection enquiries
|
||||
|
||||
For any questions regarding data protection please contact:
|
||||
|
||||
**[Data Protection Officer / Contact]**
|
||||
Email: [DPO email]
|
||||
|
||||
---
|
||||
|
||||
*This is a template — review and adapt the wording to your organisation and jurisdiction before publishing to staff.*
|
||||
Loading…
Reference in a new issue