'']; public ?int $newTagCategory = null; public array $categoryOptions = []; public string $categoryColor = 'gray'; // Language detection public bool $sessionLanguageOk = false; public bool $sessionLanguageIgnored = false; public bool $transLanguageOk = false; public bool $transLanguageIgnored = false; // Translation support public bool $translationPossible = true; public bool $translationAllowed = true; public bool $translationVisible = false; public array $translationLanguages = []; public $selectTranslationLanguage = null; public array $translationOptions = []; public $selectTagTranslation = null; public array $inputTagTranslation = []; public bool $inputDisabled = true; public $translateRadioButton = null; protected $langDetector = null; protected function rules(): array { return $this->createTagValidationRules(); } public ?int $initialTagId = null; public function mount(?int $initialTagId = null): void { $this->initialTagId = $initialTagId; if ($initialTagId) { $tag = \App\Models\Tag::find($initialTagId); if ($tag) { $color = $tag->contexts->first()?->category?->relatedColor ?? 'gray'; $tagDisplayName = $tag->translation?->name ?? $tag->name; $this->tagsArray = json_encode([[ 'value' => $tagDisplayName, 'tag_id' => $tag->tag_id, 'title' => $tagDisplayName, 'style' => '--tag-bg:' . tailwindColorToHex($color . '-400') . '; --tag-text-color:#000; --tag-hover:' . tailwindColorToHex($color . '-200'), ]]); } } $this->suggestions = $this->getSuggestions(); $this->checkTranslationAllowed(); $this->checkTranslationPossible(); } protected function getSuggestions(): array { return DB::table('taggable_tags as tt') ->join('taggable_locales as tl', 'tt.tag_id', '=', 'tl.taggable_tag_id') ->join('taggable_locale_context as tlc', 'tt.tag_id', '=', 'tlc.tag_id') ->join('taggable_contexts as tc', 'tlc.context_id', '=', 'tc.id') ->join('categories as c', 'tc.category_id', '=', 'c.id') ->join('categories as croot', DB::raw('COALESCE(c.parent_id, c.id)'), '=', 'croot.id') ->where('tl.locale', app()->getLocale()) ->select('tt.tag_id', 'tt.name', 'croot.color') ->distinct() ->orderBy('tt.name') ->get() ->map(function ($t) { $color = $t->color ?? 'gray'; return [ 'value' => $t->name, 'tag_id' => $t->tag_id, 'style' => '--tag-bg:' . tailwindColorToHex($color . '-400') . '; --tag-text-color:#000; --tag-hover:' . tailwindColorToHex($color . '-200'), 'title' => $t->name, ]; }) ->values() ->toArray(); } /** * Called from Alpine when the user selects a known tag from the whitelist. * Notifies the parent Create component. */ public function notifyTagSelected(int $tagId): void { $this->dispatch('callTagSelected', tagId: $tagId); } /** * Called from Alpine when the tag is removed (input cleared). */ public function notifyTagCleared(): void { $this->dispatch('callTagCleared'); } /** * Called from Alpine when the user types an unknown tag name and confirms it. */ public function openNewTagModal(string $name): void { $this->newTag['name'] = $name; $this->categoryOptions = $this->loadCategoryOptions(); $this->modalVisible = true; $this->checkSessionLanguage(); } protected function loadCategoryOptions(): array { return Category::where('type', Tag::class) ->get() ->map(function ($category) { return [ 'category_id' => $category->id, 'name' => ucfirst($category->translation->name ?? ''), 'description' => $category->relatedPathExSelfTranslation ?? '', 'color' => $category->relatedColor ?? 'gray', ]; }) ->sortBy('name') ->values() ->toArray(); } public function checkTranslationAllowed(): void { $allowTranslations = timebank_config('tags.allow_tag_transations_for_non_admins', false); $profileType = getActiveProfileType(); if (!$allowTranslations) { $this->translationAllowed = ($profileType === 'admin'); } else { $this->translationAllowed = true; } } public function checkTranslationPossible(): void { $profile = getActiveProfile(); if (!$profile || !method_exists($profile, 'languages')) { $this->translationPossible = false; return; } $countNonBaseLanguages = $profile->languages()->where('lang_code', '!=', timebank_config('base_language'))->count(); if ($countNonBaseLanguages === 0 && app()->getLocale() === timebank_config('base_language')) { $this->translationPossible = false; } } protected function getLanguageDetector() { if (!$this->langDetector) { $this->langDetector = new \Text_LanguageDetect(); $this->langDetector->setNameMode(2); } return $this->langDetector; } public function checkSessionLanguage(): void { $this->getLanguageDetector(); $detectedLanguage = $this->langDetector->detectSimple($this->newTag['name'] ?? ''); if ($detectedLanguage === session('locale')) { $this->sessionLanguageOk = true; $this->sessionLanguageIgnored = false; } else { $this->sessionLanguageOk = false; } $this->validateOnly('newTag.name'); } public function checkTransLanguage(): void { $this->getLanguageDetector(); $detectedLanguage = $this->langDetector->detectSimple($this->inputTagTranslation['name'] ?? ''); if ($detectedLanguage === $this->selectTranslationLanguage) { $this->transLanguageOk = true; $this->transLanguageIgnored = false; } else { $this->transLanguageOk = false; } $this->validateOnly('inputTagTranslation.name'); } public function updatedNewTagCategory(): void { $this->categoryColor = collect($this->categoryOptions) ->firstWhere('category_id', $this->newTagCategory)['color'] ?? 'gray'; $this->selectTagTranslation = null; $this->translationOptions = $this->getTranslationOptions($this->selectTranslationLanguage); $this->resetErrorBag('inputTagTranslationCategory'); } public function updatedSessionLanguageIgnored(): void { if (!$this->sessionLanguageIgnored) { $this->checkSessionLanguage(); } $this->validateOnly('newTag.name'); } public function updatedTransLanguageIgnored(): void { if (!$this->transLanguageIgnored) { $this->checkTransLanguage(); } else { $this->resetErrorBag('inputTagTranslation.name'); } } public function updatedSelectTranslationLanguage(): void { $this->selectTagTranslation = null; $this->translationOptions = $this->getTranslationOptions($this->selectTranslationLanguage); } public function updatedTranslationVisible(): void { if ($this->translationVisible && $this->translationAllowed) { $this->updatedNewTagCategory(); $profile = getActiveProfile(); if (!$profile || !method_exists($profile, 'languages')) { return; } \App\Helpers\ProfileAuthorizationHelper::authorize($profile); $this->translationLanguages = $profile ->languages() ->wherePivot('competence', 1) ->where('lang_code', '!=', app()->getLocale()) ->get() ->map(function ($language) { $language->name = trans($language->name); return $language; }) ->toArray(); if (!collect($this->translationLanguages)->contains('lang_code', 'en')) { $transLanguage = Language::where('lang_code', timebank_config('base_language'))->first(); if ($transLanguage) { $transLanguage->name = trans($transLanguage->name); $this->translationLanguages = collect($this->translationLanguages)->push($transLanguage)->toArray(); } if (app()->getLocale() != timebank_config('base_language')) { $this->selectTranslationLanguage = timebank_config('base_language'); } } } } public function updatedTranslateRadioButton(): void { if ($this->translateRadioButton === 'select') { $this->inputDisabled = true; $this->dispatch('disableInput'); } elseif ($this->translateRadioButton === 'input') { $this->inputDisabled = false; } $this->resetErrorBag('selectTagTranslation'); $this->resetErrorBag('inputTagTranslation.name'); $this->resetErrorBag('newTagCategory'); } public function updatedSelectTagTranslation(): void { if ($this->selectTagTranslation) { $this->categoryColor = Tag::find($this->selectTagTranslation)->categories->first()->relatedColor ?? 'gray'; $this->translateRadioButton = 'select'; $this->dispatch('disableInput'); } } public function updatedInputTagTranslation(): void { $this->translateRadioButton = 'input'; $this->inputDisabled = false; $this->checkTransLanguage(); } public function getTranslationOptions(?string $locale): array { if (!$locale) { return []; } $appLocale = app()->getLocale(); $contextIdsInAppLocale = DB::table('taggable_locale_context') ->whereIn('tag_id', function ($query) use ($appLocale) { $query->select('taggable_tag_id') ->from('taggable_locales') ->where('locale', $appLocale); }) ->pluck('context_id'); $tags = Tag::with(['locale', 'contexts.category']) ->whereHas('locale', function ($query) use ($locale) { $query->where('locale', $locale); }) ->whereNotIn('tag_id', function ($subquery) use ($contextIdsInAppLocale) { $subquery->select('tag_id') ->from('taggable_locale_context') ->whereIn('context_id', $contextIdsInAppLocale); }) ->get(); return $tags->map(function ($tag) use ($locale) { $category = optional($tag->contexts->first())->category; $description = optional(optional($category)->translation)->name ?? ''; return [ 'tag_id' => $tag->tag_id, 'name' => $locale == 'de' ? $tag->name : StringHelper::DutchTitleCase($tag->normalized), 'description' => $description, ]; })->sortBy('name')->values()->toArray(); } protected function createTagValidationRules(): array { return [ 'newTag.name' => array_merge( timebank_config('tags.name_rule'), timebank_config('tags.exists_in_current_locale_rule', []), [ 'sometimes', function ($attribute, $value, $fail) { if (!$this->sessionLanguageOk && !$this->sessionLanguageIgnored) { $locale = app()->getLocale(); $localeName = \Locale::getDisplayName($locale, $locale); $fail(__('Is this :locale? Please confirm here below', ['locale' => $localeName])); } }, ] ), 'newTagCategory' => function () { if ($this->translationVisible && $this->translateRadioButton == 'input') { return 'required|int'; } if (!$this->translationVisible) { return 'required|int'; } return 'nullable'; }, 'selectTagTranslation' => ($this->translationVisible && $this->translateRadioButton == 'select') ? 'required|int' : 'nullable', 'inputTagTranslation.name' => ($this->translationVisible && $this->translateRadioButton === 'input') ? array_merge( timebank_config('tags.name_rule'), timebank_config('tags.exists_in_current_locale_rule', []), [ 'sometimes', function ($attribute, $value, $fail) { if (!$this->transLanguageOk && !$this->transLanguageIgnored) { $baseLocale = $this->selectTranslationLanguage; $locale = \Locale::getDisplayName($baseLocale, $baseLocale); $fail(__('Is this :locale? Please confirm here below', ['locale' => $locale])); } }, function ($attribute, $value, $fail) { $existsInTransLationLanguage = DB::table('taggable_tags') ->join('taggable_locales', 'taggable_tags.tag_id', '=', 'taggable_locales.taggable_tag_id') ->where('taggable_locales.locale', $this->selectTranslationLanguage) ->where(function ($query) use ($value) { $query->where('taggable_tags.name', $value) ->orWhere('taggable_tags.normalized', $value); }) ->exists(); if ($existsInTransLationLanguage) { $fail(__('This tag already exists.')); } }, ] ) : 'nullable', ]; } public function createTag(): void { $this->validate($this->createTagValidationRules()); $this->resetErrorBag(); $name = app()->getLocale() == 'de' ? trim($this->newTag['name']) : StringHelper::DutchTitleCase(trim($this->newTag['name'])); $normalized = call_user_func(config('taggable.normalizer'), $name); $existing = Tag::whereHas('locale', fn ($q) => $q->where('locale', app()->getLocale())) ->where('name', $name) ->first(); if ($existing) { $tag = $existing; } else { $tag = Tag::create(['name' => $name, 'normalized' => $normalized]); $tag->locale()->create(['locale' => app()->getLocale()]); } $context = [ 'category_id' => $this->newTagCategory, 'updated_by_user' => Auth::guard('web')->id(), ]; if ($this->translationVisible) { if ($this->translateRadioButton === 'select') { $tagContext = Tag::find($this->selectTagTranslation)->contexts()->first(); $tag->contexts()->attach($tagContext->id); } elseif ($this->translateRadioButton === 'input') { $tagContext = $tag->contexts()->create($context); $this->inputTagTranslation['name'] = $this->selectTranslationLanguage == 'de' ? $this->inputTagTranslation['name'] : StringHelper::DutchTitleCase($this->inputTagTranslation['name']); $nameTranslation = $this->inputTagTranslation['name']; $normalizedTranslation = call_user_func(config('taggable.normalizer'), $nameTranslation); $tagTranslation = Tag::create([ 'name' => $nameTranslation, 'normalized' => $normalizedTranslation, ]); $tagTranslation->locale()->create(['locale' => $this->selectTranslationLanguage]); $tagTranslation->contexts()->attach($tagContext->id); } } else { if (!$tag->contexts()->where('category_id', $this->newTagCategory)->exists()) { $tag->contexts()->create($context); } } $color = collect($this->categoryOptions)->firstWhere('category_id', $this->newTagCategory)['color'] ?? 'gray'; $this->tagsArray = json_encode([[ 'value' => $tag->translation?->name ?? $tag->name, 'tag_id' => $tag->tag_id, 'title' => $name, 'style' => '--tag-bg:' . tailwindColorToHex($color . '-400') . '; --tag-text-color:#000; --tag-hover:' . tailwindColorToHex($color . '-200'), ]]); $this->modalVisible = false; $this->newTag = ['name' => '']; $this->newTagCategory = null; $this->categoryColor = 'gray'; $this->translationVisible = false; $this->translateRadioButton = null; $this->selectTagTranslation = null; $this->inputTagTranslation = []; $this->sessionLanguageOk = false; $this->sessionLanguageIgnored = false; $this->transLanguageOk = false; $this->transLanguageIgnored = false; $this->resetErrorBag(['newTag.name', 'newTagCategory']); // Reload Tagify badge in this component's input $this->dispatch('callTagifyReload', tagsArray: $this->tagsArray); // Notify parent (Create or Edit) of the selected tag $this->dispatch('callTagSelected', tagId: $tag->tag_id); SendEmailNewTag::dispatch($tag->tag_id); } public function cancelCreateTag(): void { $this->resetErrorBag(); $this->newTag = ['name' => '']; $this->newTagCategory = null; $this->categoryColor = 'gray'; $this->modalVisible = false; $this->translationVisible = false; $this->translateRadioButton = null; $this->selectTagTranslation = null; $this->inputTagTranslation = []; $this->sessionLanguageOk = false; $this->sessionLanguageIgnored = false; $this->transLanguageOk = false; $this->transLanguageIgnored = false; $this->dispatch('removeLastCallTag'); } public function render() { return view('livewire.calls.call-skill-input'); } }